Merge branch 'main' into project_search2

This commit is contained in:
Piotr Osiewicz 2023-12-11 16:47:48 +01:00
commit ddbd5cf2cb
145 changed files with 16798 additions and 12900 deletions

View file

@ -81,12 +81,11 @@ jobs:
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
- name: Set release channel to nightly, add nightly prefix to the final version
- name: Set release channel to nightly
run: |
set -eu
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
sed -i '' "s/version = \"\(.*\)\"/version = \"\1-nightly\"/" crates/zed2/Cargo.toml
echo "nightly" > crates/zed/RELEASE_CHANNEL
- name: Generate license file

43
Cargo.lock generated
View file

@ -312,6 +312,15 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "arrayref"
version = "0.3.7"
@ -1145,6 +1154,7 @@ dependencies = [
"gpui2",
"itertools 0.10.5",
"language2",
"outline2",
"project2",
"search2",
"settings2",
@ -1743,7 +1753,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.30.0"
version = "0.30.1"
dependencies = [
"anyhow",
"async-trait",
@ -3162,6 +3172,12 @@ dependencies = [
"regex",
]
[[package]]
name = "fast-srgb8"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
[[package]]
name = "fastrand"
version = "1.9.0"
@ -6263,6 +6279,28 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "palette"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2e2f34147767aa758aa649415b50a69eeb46a67f9dc7db8011eeb3d84b351dc"
dependencies = [
"approx",
"fast-srgb8",
"palette_derive",
]
[[package]]
name = "palette_derive"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7db010ec5ff3d4385e4f133916faacd9dad0f6a09394c92d825b3aed310fa0a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
name = "parity-tokio-ipc"
version = "0.9.0"
@ -9696,6 +9734,7 @@ dependencies = [
"indexmap 1.9.3",
"json_comments",
"log",
"palette",
"rust-embed",
"serde",
"simplelog",
@ -10464,7 +10503,7 @@ dependencies = [
[[package]]
name = "tree-sitter-vue"
version = "0.0.1"
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58#9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=6608d9d60c386f19d80af7d8132322fa11199c42#6608d9d60c386f19d80af7d8132322fa11199c42"
dependencies = [
"cc",
"tree-sitter",

View file

@ -205,7 +205,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
tree-sitter-lua = "0.0.14"
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42"}
tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
[patch.crates-io]

View file

@ -17,5 +17,8 @@
"file_name": "rose-pine-dawn.json",
"appearance": "light"
}
]
],
"syntax": {
"function": ["entity.name"]
}
}

View file

@ -72,16 +72,16 @@
"editor.findMatchBackground": "#6e6a8626",
"editor.findMatchHighlightBackground": "#6e6a8626",
"editor.findRangeHighlightBackground": "#6e6a8626",
"editor.findRangeHighlightBorder": "#000000",
"editor.findRangeHighlightBorder": "#0000",
"editor.focusedStackFrameHighlightBackground": "#6e6a8614",
"editor.foldBackground": "#fffaf3",
"editor.foreground": "#575279",
"editor.hoverHighlightBackground": "#000000",
"editor.hoverHighlightBackground": "#0000",
"editor.inactiveSelectionBackground": "#6e6a860d",
"editor.inlineValuesBackground": "#000000",
"editor.inlineValuesBackground": "#0000",
"editor.inlineValuesForeground": "#797593",
"editor.lineHighlightBackground": "#6e6a860d",
"editor.lineHighlightBorder": "#000000",
"editor.lineHighlightBorder": "#0000",
"editor.linkedEditingBackground": "#fffaf3",
"editor.rangeHighlightBackground": "#6e6a860d",
"editor.selectionBackground": "#6e6a8614",
@ -94,9 +94,9 @@
"editor.snippetTabstopHighlightBorder": "#fffaf3",
"editor.stackFrameHighlightBackground": "#6e6a8614",
"editor.symbolHighlightBackground": "#6e6a8614",
"editor.symbolHighlightBorder": "#000000",
"editor.symbolHighlightBorder": "#0000",
"editor.wordHighlightBackground": "#6e6a8614",
"editor.wordHighlightBorder": "#000000",
"editor.wordHighlightBorder": "#0000",
"editor.wordHighlightStrongBackground": "#6e6a8614",
"editor.wordHighlightStrongBorder": "#6e6a8614",
"editorBracketHighlight.foreground1": "#b4637a80",
@ -105,7 +105,7 @@
"editorBracketHighlight.foreground4": "#56949f80",
"editorBracketHighlight.foreground5": "#d7827e80",
"editorBracketHighlight.foreground6": "#907aa980",
"editorBracketMatch.background": "#000000",
"editorBracketMatch.background": "#0000",
"editorBracketMatch.border": "#797593",
"editorBracketPairGuide.activeBackground1": "#286983",
"editorBracketPairGuide.activeBackground2": "#d7827e",
@ -122,29 +122,29 @@
"editorCodeLens.foreground": "#d7827e",
"editorCursor.background": "#575279",
"editorCursor.foreground": "#9893a5",
"editorError.border": "#000000",
"editorError.border": "#0000",
"editorError.foreground": "#b4637a",
"editorGhostText.foreground": "#797593",
"editorGroup.border": "#000000",
"editorGroup.border": "#0000",
"editorGroup.dropBackground": "#fffaf3",
"editorGroup.emptyBackground": "#000000",
"editorGroup.focusedEmptyBorder": "#000000",
"editorGroupHeader.noTabsBackground": "#000000",
"editorGroupHeader.tabsBackground": "#000000",
"editorGroupHeader.tabsBorder": "#000000",
"editorGroup.emptyBackground": "#0000",
"editorGroup.focusedEmptyBorder": "#0000",
"editorGroupHeader.noTabsBackground": "#0000",
"editorGroupHeader.tabsBackground": "#0000",
"editorGroupHeader.tabsBorder": "#0000",
"editorGutter.addedBackground": "#56949f",
"editorGutter.background": "#faf4ed",
"editorGutter.commentRangeForeground": "#797593",
"editorGutter.deletedBackground": "#b4637a",
"editorGutter.foldingControlForeground": "#907aa9",
"editorGutter.modifiedBackground": "#d7827e",
"editorHint.border": "#000000",
"editorHint.border": "#0000",
"editorHint.foreground": "#797593",
"editorHoverWidget.background": "#fffaf3",
"editorHoverWidget.border": "#9893a580",
"editorHoverWidget.foreground": "#797593",
"editorHoverWidget.highlightForeground": "#575279",
"editorHoverWidget.statusBarBackground": "#000000",
"editorHoverWidget.statusBarBackground": "#0000",
"editorIndentGuide.activeBackground": "#9893a5",
"editorIndentGuide.background": "#6e6a8626",
"editorInfo.border": "#f2e9e1",
@ -181,19 +181,19 @@
"editorOverviewRuler.warningForeground": "#ea9d3480",
"editorOverviewRuler.wordHighlightForeground": "#6e6a8614",
"editorOverviewRuler.wordHighlightStrongForeground": "#6e6a8626",
"editorPane.background": "#000000",
"editorPane.background": "#0000",
"editorRuler.foreground": "#6e6a8626",
"editorSuggestWidget.background": "#fffaf3",
"editorSuggestWidget.border": "#000000",
"editorSuggestWidget.border": "#0000",
"editorSuggestWidget.focusHighlightForeground": "#d7827e",
"editorSuggestWidget.foreground": "#797593",
"editorSuggestWidget.highlightForeground": "#d7827e",
"editorSuggestWidget.selectedBackground": "#6e6a8614",
"editorSuggestWidget.selectedForeground": "#575279",
"editorSuggestWidget.selectedIconForeground": "#575279",
"editorUnnecessaryCode.border": "#000000",
"editorUnnecessaryCode.border": "#0000",
"editorUnnecessaryCode.opacity": "#57527980",
"editorWarning.border": "#000000",
"editorWarning.border": "#0000",
"editorWarning.foreground": "#ea9d34",
"editorWhitespace.foreground": "#9893a5",
"editorWidget.background": "#fffaf3",
@ -310,7 +310,7 @@
"notificationsWarningIcon.foreground": "#ea9d34",
"notificationToast.border": "#6e6a8614",
"panel.background": "#fffaf3",
"panel.border": "#000000",
"panel.border": "#0000",
"panel.dropBorder": "#f2e9e1",
"panelInput.border": "#fffaf3",
"panelSection.dropBackground": "#6e6a8614",
@ -356,7 +356,7 @@
"sideBar.background": "#faf4ed",
"sideBar.dropBackground": "#fffaf3",
"sideBar.foreground": "#797593",
"sideBarSectionHeader.background": "#000000",
"sideBarSectionHeader.background": "#0000",
"sideBarSectionHeader.border": "#6e6a8614",
"statusBar.background": "#faf4ed",
"statusBar.debuggingBackground": "#907aa9",
@ -408,15 +408,15 @@
"tab.activeBackground": "#6e6a860d",
"tab.activeForeground": "#575279",
"tab.activeModifiedBorder": "#56949f",
"tab.border": "#000000",
"tab.border": "#0000",
"tab.hoverBackground": "#6e6a8614",
"tab.inactiveBackground": "#000000",
"tab.inactiveBackground": "#0000",
"tab.inactiveForeground": "#797593",
"tab.inactiveModifiedBorder": "#56949f80",
"tab.lastPinnedBorder": "#9893a5",
"tab.unfocusedActiveBackground": "#000000",
"tab.unfocusedHoverBackground": "#000000",
"tab.unfocusedInactiveBackground": "#000000",
"tab.unfocusedActiveBackground": "#0000",
"tab.unfocusedHoverBackground": "#0000",
"tab.unfocusedInactiveBackground": "#0000",
"tab.unfocusedInactiveModifiedBorder": "#56949f80",
"terminal.ansiBlack": "#f2e9e1",
"terminal.ansiBlue": "#56949f",

View file

@ -72,16 +72,16 @@
"editor.findMatchBackground": "#817c9c4d",
"editor.findMatchHighlightBackground": "#817c9c4d",
"editor.findRangeHighlightBackground": "#817c9c4d",
"editor.findRangeHighlightBorder": "#000000",
"editor.findRangeHighlightBorder": "#0000",
"editor.focusedStackFrameHighlightBackground": "#817c9c26",
"editor.foldBackground": "#2a273f",
"editor.foreground": "#e0def4",
"editor.hoverHighlightBackground": "#000000",
"editor.hoverHighlightBackground": "#0000",
"editor.inactiveSelectionBackground": "#817c9c14",
"editor.inlineValuesBackground": "#000000",
"editor.inlineValuesBackground": "#0000",
"editor.inlineValuesForeground": "#908caa",
"editor.lineHighlightBackground": "#817c9c14",
"editor.lineHighlightBorder": "#000000",
"editor.lineHighlightBorder": "#0000",
"editor.linkedEditingBackground": "#2a273f",
"editor.rangeHighlightBackground": "#817c9c14",
"editor.selectionBackground": "#817c9c26",
@ -94,9 +94,9 @@
"editor.snippetTabstopHighlightBorder": "#2a273f",
"editor.stackFrameHighlightBackground": "#817c9c26",
"editor.symbolHighlightBackground": "#817c9c26",
"editor.symbolHighlightBorder": "#000000",
"editor.symbolHighlightBorder": "#0000",
"editor.wordHighlightBackground": "#817c9c26",
"editor.wordHighlightBorder": "#000000",
"editor.wordHighlightBorder": "#0000",
"editor.wordHighlightStrongBackground": "#817c9c26",
"editor.wordHighlightStrongBorder": "#817c9c26",
"editorBracketHighlight.foreground1": "#eb6f9280",
@ -105,7 +105,7 @@
"editorBracketHighlight.foreground4": "#9ccfd880",
"editorBracketHighlight.foreground5": "#ea9a9780",
"editorBracketHighlight.foreground6": "#c4a7e780",
"editorBracketMatch.background": "#000000",
"editorBracketMatch.background": "#0000",
"editorBracketMatch.border": "#908caa",
"editorBracketPairGuide.activeBackground1": "#3e8fb0",
"editorBracketPairGuide.activeBackground2": "#ea9a97",
@ -122,29 +122,29 @@
"editorCodeLens.foreground": "#ea9a97",
"editorCursor.background": "#e0def4",
"editorCursor.foreground": "#6e6a86",
"editorError.border": "#000000",
"editorError.border": "#0000",
"editorError.foreground": "#eb6f92",
"editorGhostText.foreground": "#908caa",
"editorGroup.border": "#000000",
"editorGroup.border": "#0000",
"editorGroup.dropBackground": "#2a273f",
"editorGroup.emptyBackground": "#000000",
"editorGroup.focusedEmptyBorder": "#000000",
"editorGroupHeader.noTabsBackground": "#000000",
"editorGroupHeader.tabsBackground": "#000000",
"editorGroupHeader.tabsBorder": "#000000",
"editorGroup.emptyBackground": "#0000",
"editorGroup.focusedEmptyBorder": "#0000",
"editorGroupHeader.noTabsBackground": "#0000",
"editorGroupHeader.tabsBackground": "#0000",
"editorGroupHeader.tabsBorder": "#0000",
"editorGutter.addedBackground": "#9ccfd8",
"editorGutter.background": "#232136",
"editorGutter.commentRangeForeground": "#908caa",
"editorGutter.deletedBackground": "#eb6f92",
"editorGutter.foldingControlForeground": "#c4a7e7",
"editorGutter.modifiedBackground": "#ea9a97",
"editorHint.border": "#000000",
"editorHint.border": "#0000",
"editorHint.foreground": "#908caa",
"editorHoverWidget.background": "#2a273f",
"editorHoverWidget.border": "#6e6a8680",
"editorHoverWidget.foreground": "#908caa",
"editorHoverWidget.highlightForeground": "#e0def4",
"editorHoverWidget.statusBarBackground": "#000000",
"editorHoverWidget.statusBarBackground": "#0000",
"editorIndentGuide.activeBackground": "#6e6a86",
"editorIndentGuide.background": "#817c9c4d",
"editorInfo.border": "#393552",
@ -181,19 +181,19 @@
"editorOverviewRuler.warningForeground": "#f6c17780",
"editorOverviewRuler.wordHighlightForeground": "#817c9c26",
"editorOverviewRuler.wordHighlightStrongForeground": "#817c9c4d",
"editorPane.background": "#000000",
"editorPane.background": "#0000",
"editorRuler.foreground": "#817c9c4d",
"editorSuggestWidget.background": "#2a273f",
"editorSuggestWidget.border": "#000000",
"editorSuggestWidget.border": "#0000",
"editorSuggestWidget.focusHighlightForeground": "#ea9a97",
"editorSuggestWidget.foreground": "#908caa",
"editorSuggestWidget.highlightForeground": "#ea9a97",
"editorSuggestWidget.selectedBackground": "#817c9c26",
"editorSuggestWidget.selectedForeground": "#e0def4",
"editorSuggestWidget.selectedIconForeground": "#e0def4",
"editorUnnecessaryCode.border": "#000000",
"editorUnnecessaryCode.border": "#0000",
"editorUnnecessaryCode.opacity": "#e0def480",
"editorWarning.border": "#000000",
"editorWarning.border": "#0000",
"editorWarning.foreground": "#f6c177",
"editorWhitespace.foreground": "#6e6a86",
"editorWidget.background": "#2a273f",
@ -310,7 +310,7 @@
"notificationsWarningIcon.foreground": "#f6c177",
"notificationToast.border": "#817c9c26",
"panel.background": "#2a273f",
"panel.border": "#000000",
"panel.border": "#0000",
"panel.dropBorder": "#393552",
"panelInput.border": "#2a273f",
"panelSection.dropBackground": "#817c9c26",
@ -356,7 +356,7 @@
"sideBar.background": "#232136",
"sideBar.dropBackground": "#2a273f",
"sideBar.foreground": "#908caa",
"sideBarSectionHeader.background": "#000000",
"sideBarSectionHeader.background": "#0000",
"sideBarSectionHeader.border": "#817c9c26",
"statusBar.background": "#232136",
"statusBar.debuggingBackground": "#c4a7e7",
@ -408,15 +408,15 @@
"tab.activeBackground": "#817c9c14",
"tab.activeForeground": "#e0def4",
"tab.activeModifiedBorder": "#9ccfd8",
"tab.border": "#000000",
"tab.border": "#0000",
"tab.hoverBackground": "#817c9c26",
"tab.inactiveBackground": "#000000",
"tab.inactiveBackground": "#0000",
"tab.inactiveForeground": "#908caa",
"tab.inactiveModifiedBorder": "#9ccfd880",
"tab.lastPinnedBorder": "#6e6a86",
"tab.unfocusedActiveBackground": "#000000",
"tab.unfocusedHoverBackground": "#000000",
"tab.unfocusedInactiveBackground": "#000000",
"tab.unfocusedActiveBackground": "#0000",
"tab.unfocusedHoverBackground": "#0000",
"tab.unfocusedInactiveBackground": "#0000",
"tab.unfocusedInactiveModifiedBorder": "#9ccfd880",
"terminal.ansiBlack": "#393552",
"terminal.ansiBlue": "#9ccfd8",

View file

@ -72,16 +72,16 @@
"editor.findMatchBackground": "#6e6a8666",
"editor.findMatchHighlightBackground": "#6e6a8666",
"editor.findRangeHighlightBackground": "#6e6a8666",
"editor.findRangeHighlightBorder": "#000000",
"editor.findRangeHighlightBorder": "#0000",
"editor.focusedStackFrameHighlightBackground": "#6e6a8633",
"editor.foldBackground": "#1f1d2e",
"editor.foreground": "#e0def4",
"editor.hoverHighlightBackground": "#000000",
"editor.hoverHighlightBackground": "#0000",
"editor.inactiveSelectionBackground": "#6e6a861a",
"editor.inlineValuesBackground": "#000000",
"editor.inlineValuesBackground": "#0000",
"editor.inlineValuesForeground": "#908caa",
"editor.lineHighlightBackground": "#6e6a861a",
"editor.lineHighlightBorder": "#000000",
"editor.lineHighlightBorder": "#0000",
"editor.linkedEditingBackground": "#1f1d2e",
"editor.rangeHighlightBackground": "#6e6a861a",
"editor.selectionBackground": "#6e6a8633",
@ -94,9 +94,9 @@
"editor.snippetTabstopHighlightBorder": "#1f1d2e",
"editor.stackFrameHighlightBackground": "#6e6a8633",
"editor.symbolHighlightBackground": "#6e6a8633",
"editor.symbolHighlightBorder": "#000000",
"editor.symbolHighlightBorder": "#0000",
"editor.wordHighlightBackground": "#6e6a8633",
"editor.wordHighlightBorder": "#000000",
"editor.wordHighlightBorder": "#0000",
"editor.wordHighlightStrongBackground": "#6e6a8633",
"editor.wordHighlightStrongBorder": "#6e6a8633",
"editorBracketHighlight.foreground1": "#eb6f9280",
@ -105,7 +105,7 @@
"editorBracketHighlight.foreground4": "#9ccfd880",
"editorBracketHighlight.foreground5": "#ebbcba80",
"editorBracketHighlight.foreground6": "#c4a7e780",
"editorBracketMatch.background": "#000000",
"editorBracketMatch.background": "#0000",
"editorBracketMatch.border": "#908caa",
"editorBracketPairGuide.activeBackground1": "#31748f",
"editorBracketPairGuide.activeBackground2": "#ebbcba",
@ -122,29 +122,29 @@
"editorCodeLens.foreground": "#ebbcba",
"editorCursor.background": "#e0def4",
"editorCursor.foreground": "#6e6a86",
"editorError.border": "#000000",
"editorError.border": "#0000",
"editorError.foreground": "#eb6f92",
"editorGhostText.foreground": "#908caa",
"editorGroup.border": "#000000",
"editorGroup.border": "#0000",
"editorGroup.dropBackground": "#1f1d2e",
"editorGroup.emptyBackground": "#000000",
"editorGroup.focusedEmptyBorder": "#000000",
"editorGroupHeader.noTabsBackground": "#000000",
"editorGroupHeader.tabsBackground": "#000000",
"editorGroupHeader.tabsBorder": "#000000",
"editorGroup.emptyBackground": "#0000",
"editorGroup.focusedEmptyBorder": "#0000",
"editorGroupHeader.noTabsBackground": "#0000",
"editorGroupHeader.tabsBackground": "#0000",
"editorGroupHeader.tabsBorder": "#0000",
"editorGutter.addedBackground": "#9ccfd8",
"editorGutter.background": "#191724",
"editorGutter.commentRangeForeground": "#908caa",
"editorGutter.deletedBackground": "#eb6f92",
"editorGutter.foldingControlForeground": "#c4a7e7",
"editorGutter.modifiedBackground": "#ebbcba",
"editorHint.border": "#000000",
"editorHint.border": "#0000",
"editorHint.foreground": "#908caa",
"editorHoverWidget.background": "#1f1d2e",
"editorHoverWidget.border": "#6e6a8680",
"editorHoverWidget.foreground": "#908caa",
"editorHoverWidget.highlightForeground": "#e0def4",
"editorHoverWidget.statusBarBackground": "#000000",
"editorHoverWidget.statusBarBackground": "#0000",
"editorIndentGuide.activeBackground": "#6e6a86",
"editorIndentGuide.background": "#6e6a8666",
"editorInfo.border": "#26233a",
@ -181,19 +181,19 @@
"editorOverviewRuler.warningForeground": "#f6c17780",
"editorOverviewRuler.wordHighlightForeground": "#6e6a8633",
"editorOverviewRuler.wordHighlightStrongForeground": "#6e6a8666",
"editorPane.background": "#000000",
"editorPane.background": "#0000",
"editorRuler.foreground": "#6e6a8666",
"editorSuggestWidget.background": "#1f1d2e",
"editorSuggestWidget.border": "#000000",
"editorSuggestWidget.border": "#0000",
"editorSuggestWidget.focusHighlightForeground": "#ebbcba",
"editorSuggestWidget.foreground": "#908caa",
"editorSuggestWidget.highlightForeground": "#ebbcba",
"editorSuggestWidget.selectedBackground": "#6e6a8633",
"editorSuggestWidget.selectedForeground": "#e0def4",
"editorSuggestWidget.selectedIconForeground": "#e0def4",
"editorUnnecessaryCode.border": "#000000",
"editorUnnecessaryCode.border": "#0000",
"editorUnnecessaryCode.opacity": "#e0def480",
"editorWarning.border": "#000000",
"editorWarning.border": "#0000",
"editorWarning.foreground": "#f6c177",
"editorWhitespace.foreground": "#6e6a86",
"editorWidget.background": "#1f1d2e",
@ -310,7 +310,7 @@
"notificationsWarningIcon.foreground": "#f6c177",
"notificationToast.border": "#6e6a8633",
"panel.background": "#1f1d2e",
"panel.border": "#000000",
"panel.border": "#0000",
"panel.dropBorder": "#26233a",
"panelInput.border": "#1f1d2e",
"panelSection.dropBackground": "#6e6a8633",
@ -356,7 +356,7 @@
"sideBar.background": "#191724",
"sideBar.dropBackground": "#1f1d2e",
"sideBar.foreground": "#908caa",
"sideBarSectionHeader.background": "#000000",
"sideBarSectionHeader.background": "#0000",
"sideBarSectionHeader.border": "#6e6a8633",
"statusBar.background": "#191724",
"statusBar.debuggingBackground": "#c4a7e7",
@ -408,15 +408,15 @@
"tab.activeBackground": "#6e6a861a",
"tab.activeForeground": "#e0def4",
"tab.activeModifiedBorder": "#9ccfd8",
"tab.border": "#000000",
"tab.border": "#0000",
"tab.hoverBackground": "#6e6a8633",
"tab.inactiveBackground": "#000000",
"tab.inactiveBackground": "#0000",
"tab.inactiveForeground": "#908caa",
"tab.inactiveModifiedBorder": "#9ccfd880",
"tab.lastPinnedBorder": "#6e6a86",
"tab.unfocusedActiveBackground": "#000000",
"tab.unfocusedHoverBackground": "#000000",
"tab.unfocusedInactiveBackground": "#000000",
"tab.unfocusedActiveBackground": "#0000",
"tab.unfocusedHoverBackground": "#0000",
"tab.unfocusedInactiveBackground": "#0000",
"tab.unfocusedInactiveModifiedBorder": "#9ccfd880",
"terminal.ansiBlack": "#26233a",
"terminal.ansiBlue": "#9ccfd8",

View file

@ -14,7 +14,7 @@ use ui::h_stack;
use util::ResultExt;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(ShowErrorMessage);
actions!(activity_indicator, [ShowErrorMessage]);
const DOWNLOAD_ICON: &str = "icons/download.svg";
const WARNING_ICON: &str = "icons/warning.svg";

View file

@ -12,13 +12,15 @@ use chrono::{DateTime, Local};
use collections::HashMap;
use fs::Fs;
use futures::StreamExt;
use gpui::{actions, AppContext};
use gpui::{actions, AppContext, SharedString};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
use util::paths::CONVERSATIONS_DIR;
actions!(
assistant,
[
NewConversation,
Assist,
Split,
@ -29,6 +31,7 @@ actions!(
InlineAssist,
ToggleIncludeConversation,
ToggleRetrieveContext,
]
);
#[derive(
@ -47,7 +50,7 @@ struct MessageMetadata {
enum MessageStatus {
Pending,
Done,
Error(Arc<str>),
Error(SharedString),
}
#[derive(Serialize, Deserialize)]

View file

@ -1628,8 +1628,9 @@ impl Conversation {
metadata.status = MessageStatus::Done;
}
Err(error) => {
metadata.status =
MessageStatus::Error(error.to_string().trim().into());
metadata.status = MessageStatus::Error(SharedString::from(
error.to_string().trim().to_string(),
));
}
}
cx.notify();
@ -2273,7 +2274,7 @@ impl ConversationEditor {
Some(
div()
.id("error")
.tooltip(move |cx| Tooltip::text(&error, cx))
.tooltip(move |cx| Tooltip::text(error.clone(), cx))
.child(IconElement::new(Icon::XCircle)),
)
} else {

View file

@ -26,10 +26,13 @@ const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
//todo!(remove CheckThatAutoUpdaterWorks)
actions!(
auto_update,
[
Check,
DismissErrorMessage,
ViewReleaseNotes,
CheckThatAutoUpdaterWorks
]
);
#[derive(Serialize)]
@ -85,15 +88,7 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
AutoUpdateSetting::register(cx);
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace
.register_action(|_, action: &Check, cx| check(action, cx))
.register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
cx.spawn(|_, _cx| async move {
prompt.await.ok();
})
.detach();
});
workspace.register_action(|_, action: &Check, cx| check(action, cx));
// @nate - code to trigger update notification on launch
// workspace.show_notification(0, _cx, |cx| {
@ -130,9 +125,15 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
}
}
pub fn check(_: &Check, cx: &mut AppContext) {
pub fn check(_: &Check, cx: &mut ViewContext<Workspace>) {
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
} else {
drop(cx.prompt(
gpui::PromptLevel::Info,
"Auto-updates disabled for non-bundled app.",
&["Ok"],
));
}
}

View file

@ -19,7 +19,7 @@ search = { package = "search2", path = "../search2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
workspace = { package = "workspace2", path = "../workspace2" }
# outline = { path = "../outline" }
outline = { package = "outline2", path = "../outline2" }
itertools = "0.10"
[dev-dependencies]

View file

@ -1,13 +1,14 @@
use editor::Editor;
use gpui::{
Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
ViewContext, WeakView,
ViewContext,
};
use itertools::Itertools;
use theme::ActiveTheme;
use ui::{prelude::*, ButtonLike, ButtonStyle, Label};
use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
use workspace::{
item::{ItemEvent, ItemHandle},
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
};
pub enum Event {
@ -18,16 +19,14 @@ pub struct Breadcrumbs {
pane_focused: bool,
active_item: Option<Box<dyn ItemHandle>>,
subscription: Option<Subscription>,
_workspace: WeakView<Workspace>,
}
impl Breadcrumbs {
pub fn new(workspace: &Workspace) -> Self {
pub fn new() -> Self {
Self {
pane_focused: false,
active_item: Default::default(),
subscription: Default::default(),
_workspace: workspace.weak_handle(),
}
}
}
@ -62,97 +61,24 @@ impl Render for Breadcrumbs {
Label::new("").into_any_element()
});
let editor = active_item
.downcast::<Editor>()
.map(|editor| editor.downgrade());
element.child(
ButtonLike::new("toggle outline view")
.style(ButtonStyle::Subtle)
.child(h_stack().gap_1().children(breadcrumbs))
// We disable the button when it is not focused
// due to ... @julia what was the reason again?
.disabled(!self.pane_focused)
.on_click(move |_, _cx| {
todo!("outline::toggle");
// this.update(cx, |this, cx| {
// if let Some(workspace) = this.workspace.upgrade() {
// workspace.update(cx, |_workspace, _cx| {
// outline::toggle(workspace, &Default::default(), cx)
// })
// }
// })
// .ok();
}),
.on_click(move |_, cx| {
if let Some(editor) = editor.as_ref().and_then(|editor| editor.upgrade()) {
outline::toggle(editor, &outline::Toggle, cx)
}
})
.tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
)
}
}
// impl View for Breadcrumbs {
// fn ui_name() -> &'static str {
// "Breadcrumbs"
// }
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
// let active_item = match &self.active_item {
// Some(active_item) => active_item,
// None => return Empty::new().into_any(),
// };
// let not_editor = active_item.downcast::<editor::Editor>().is_none();
// let theme = theme::current(cx).clone();
// let style = &theme.workspace.toolbar.breadcrumbs;
// let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
// Some(breadcrumbs) => breadcrumbs,
// None => return Empty::new().into_any(),
// }
// .into_iter()
// .map(|breadcrumb| {
// Text::new(
// breadcrumb.text,
// theme.workspace.toolbar.breadcrumbs.default.text.clone(),
// )
// .with_highlights(breadcrumb.highlights.unwrap_or_default())
// .into_any()
// });
// let crumbs = Flex::row()
// .with_children(Itertools::intersperse_with(breadcrumbs, || {
// Label::new(" ", style.default.text.clone()).into_any()
// }))
// .constrained()
// .with_height(theme.workspace.toolbar.breadcrumb_height)
// .contained();
// if not_editor || !self.pane_focused {
// return crumbs
// .with_style(style.default.container)
// .aligned()
// .left()
// .into_any();
// }
// MouseEventHandler::new::<Breadcrumbs, _>(0, cx, |state, _| {
// let style = style.style_for(state);
// crumbs.with_style(style.container)
// })
// .on_click(MouseButton::Left, |_, this, cx| {
// if let Some(workspace) = this.workspace.upgrade(cx) {
// workspace.update(cx, |workspace, cx| {
// outline::toggle(workspace, &Default::default(), cx)
// })
// }
// })
// .with_tooltip::<Breadcrumbs>(
// 0,
// "Show symbol outline".to_owned(),
// Some(Box::new(outline::Toggle)),
// theme.tooltip.clone(),
// cx,
// )
// .aligned()
// .left()
// .into_any()
// }
// }
impl ToolbarItemView for Breadcrumbs {
fn set_active_pane_item(
&mut self,

View file

@ -8,7 +8,8 @@ use collections::{hash_map, HashMap, HashSet};
use db::RELEASE_CHANNEL;
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
WeakModel,
};
use rpc::{
proto::{self, ChannelVisibility},
@ -46,7 +47,7 @@ pub struct ChannelStore {
#[derive(Clone, Debug, PartialEq)]
pub struct Channel {
pub id: ChannelId,
pub name: String,
pub name: SharedString,
pub visibility: proto::ChannelVisibility,
pub role: proto::ChannelRole,
pub unseen_note_version: Option<(u64, clock::Global)>,
@ -895,14 +896,16 @@ impl ChannelStore {
.channel_invitations
.binary_search_by_key(&channel.id, |c| c.id)
{
Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
Ok(ix) => {
Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into()
}
Err(ix) => self.channel_invitations.insert(
ix,
Arc::new(Channel {
id: channel.id,
visibility: channel.visibility(),
role: channel.role(),
name: channel.name,
name: channel.name.into(),
unseen_note_version: None,
unseen_message_id: None,
parent_path: channel.parent_path,

View file

@ -104,7 +104,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
existing_channel.visibility = channel_proto.visibility();
existing_channel.role = channel_proto.role();
existing_channel.name = channel_proto.name;
existing_channel.name = channel_proto.name.into();
} else {
self.channels_by_id.insert(
channel_proto.id,
@ -112,7 +112,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
id: channel_proto.id,
visibility: channel_proto.visibility(),
role: channel_proto.role(),
name: channel_proto.name,
name: channel_proto.name.into(),
unseen_note_version: None,
unseen_message_id: None,
parent_path: channel_proto.parent_path,
@ -146,11 +146,11 @@ fn channel_path_sorting_key<'a>(
let (parent_path, name) = channels_by_id
.get(&id)
.map_or((&[] as &[_], None), |channel| {
(channel.parent_path.as_slice(), Some(channel.name.as_str()))
(channel.parent_path.as_slice(), Some(channel.name.as_ref()))
});
parent_path
.iter()
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_str()))
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
.chain(name)
}

View file

@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(SignIn, SignOut, Reconnect);
actions!(client, [SignIn, SignOut, Reconnect]);
pub fn init_settings(cx: &mut AppContext) {
TelemetrySettings::register(cx);

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.30.0"
version = "0.30.1"
publish = false
[[bin]]

View file

@ -116,12 +116,13 @@ struct CreateUserResponse {
#[derive(Debug, Deserialize)]
struct Panic {
version: String,
release_channel: String,
text: String,
}
#[instrument(skip(panic))]
async fn trace_panic(panic: Json<Panic>) -> Result<()> {
tracing::error!(version = %panic.version, text = %panic.text, "panic report");
tracing::error!(version = %panic.version, release_channel = %panic.release_channel, text = %panic.text, "panic report");
Ok(())
}

View file

@ -7,7 +7,7 @@ use call::ActiveCall;
use channel::{ChannelId, ChannelMembership, ChannelStore};
use client::User;
use futures::future::try_join_all;
use gpui::{BackgroundExecutor, Model, TestAppContext};
use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext};
use rpc::{
proto::{self, ChannelRole},
RECEIVE_TIMEOUT,
@ -46,13 +46,13 @@ async fn test_core_channels(
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".to_string(),
name: "channel-b".into(),
depth: 1,
role: ChannelRole::Admin,
},
@ -92,7 +92,7 @@ async fn test_core_channels(
cx_b,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Member,
}],
@ -140,13 +140,13 @@ async fn test_core_channels(
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
name: "channel-a".into(),
role: ChannelRole::Member,
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".to_string(),
name: "channel-b".into(),
role: ChannelRole::Member,
depth: 1,
},
@ -168,19 +168,19 @@ async fn test_core_channels(
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
name: "channel-a".into(),
role: ChannelRole::Member,
depth: 0,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".to_string(),
name: "channel-b".into(),
role: ChannelRole::Member,
depth: 1,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".to_string(),
name: "channel-c".into(),
role: ChannelRole::Member,
depth: 2,
},
@ -211,19 +211,19 @@ async fn test_core_channels(
&[
ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
},
ExpectedChannel {
id: channel_b_id,
name: "channel-b".to_string(),
name: "channel-b".into(),
depth: 1,
role: ChannelRole::Admin,
},
ExpectedChannel {
id: channel_c_id,
name: "channel-c".to_string(),
name: "channel-c".into(),
depth: 2,
role: ChannelRole::Admin,
},
@ -245,7 +245,7 @@ async fn test_core_channels(
cx_a,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
}],
@ -255,7 +255,7 @@ async fn test_core_channels(
cx_b,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
}],
@ -278,7 +278,7 @@ async fn test_core_channels(
cx_a,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a".to_string(),
name: "channel-a".into(),
depth: 0,
role: ChannelRole::Admin,
}],
@ -309,7 +309,7 @@ async fn test_core_channels(
cx_a,
&[ExpectedChannel {
id: channel_a_id,
name: "channel-a-renamed".to_string(),
name: "channel-a-renamed".into(),
depth: 0,
role: ChannelRole::Admin,
}],
@ -418,7 +418,7 @@ async fn test_channel_room(
cx_b,
&[ExpectedChannel {
id: zed_id,
name: "zed".to_string(),
name: "zed".into(),
depth: 0,
role: ChannelRole::Member,
}],
@ -680,7 +680,7 @@ async fn test_permissions_update_while_invited(
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust".to_string(),
name: "rust".into(),
role: ChannelRole::Member,
}],
);
@ -708,7 +708,7 @@ async fn test_permissions_update_while_invited(
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust".to_string(),
name: "rust".into(),
role: ChannelRole::Member,
}],
);
@ -747,7 +747,7 @@ async fn test_channel_rename(
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust-archive".to_string(),
name: "rust-archive".into(),
role: ChannelRole::Admin,
}],
);
@ -759,7 +759,7 @@ async fn test_channel_rename(
&[ExpectedChannel {
depth: 0,
id: rust_id,
name: "rust-archive".to_string(),
name: "rust-archive".into(),
role: ChannelRole::Member,
}],
);
@ -888,7 +888,7 @@ async fn test_lost_channel_creation(
&[ExpectedChannel {
depth: 0,
id: channel_id,
name: "x".to_string(),
name: "x".into(),
role: ChannelRole::Member,
}],
);
@ -912,13 +912,13 @@ async fn test_lost_channel_creation(
ExpectedChannel {
depth: 0,
id: channel_id,
name: "x".to_string(),
name: "x".into(),
role: ChannelRole::Admin,
},
ExpectedChannel {
depth: 1,
id: subchannel_id,
name: "subchannel".to_string(),
name: "subchannel".into(),
role: ChannelRole::Admin,
},
],
@ -943,13 +943,13 @@ async fn test_lost_channel_creation(
ExpectedChannel {
depth: 0,
id: channel_id,
name: "x".to_string(),
name: "x".into(),
role: ChannelRole::Member,
},
ExpectedChannel {
depth: 1,
id: subchannel_id,
name: "subchannel".to_string(),
name: "subchannel".into(),
role: ChannelRole::Member,
},
],
@ -1221,13 +1221,13 @@ async fn test_channel_membership_notifications(
ExpectedChannel {
depth: 0,
id: zed_channel,
name: "zed".to_string(),
name: "zed".into(),
role: ChannelRole::Guest,
},
ExpectedChannel {
depth: 1,
id: vim_channel,
name: "vim".to_string(),
name: "vim".into(),
role: ChannelRole::Member,
},
],
@ -1250,13 +1250,13 @@ async fn test_channel_membership_notifications(
ExpectedChannel {
depth: 0,
id: zed_channel,
name: "zed".to_string(),
name: "zed".into(),
role: ChannelRole::Guest,
},
ExpectedChannel {
depth: 1,
id: vim_channel,
name: "vim".to_string(),
name: "vim".into(),
role: ChannelRole::Guest,
},
],
@ -1476,7 +1476,7 @@ async fn test_channel_moving(
struct ExpectedChannel {
depth: usize,
id: ChannelId,
name: String,
name: SharedString,
role: ChannelRole,
}
@ -1515,7 +1515,7 @@ fn assert_channels(
.ordered_channels()
.map(|(depth, channel)| ExpectedChannel {
depth,
name: channel.name.clone(),
name: channel.name.clone().into(),
id: channel.id,
role: channel.role,
})

View file

@ -3,7 +3,7 @@ use crate::db::ChannelRole;
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
use anyhow::Result;
use async_trait::async_trait;
use gpui::{BackgroundExecutor, TestAppContext};
use gpui::{BackgroundExecutor, SharedString, TestAppContext};
use rand::prelude::*;
use serde_derive::{Deserialize, Serialize};
use std::{
@ -30,13 +30,13 @@ struct RandomChannelBufferTest;
#[derive(Clone, Serialize, Deserialize)]
enum ChannelBufferOperation {
JoinChannelNotes {
channel_name: String,
channel_name: SharedString,
},
LeaveChannelNotes {
channel_name: String,
channel_name: SharedString,
},
EditChannelNotes {
channel_name: String,
channel_name: SharedString,
edits: Vec<(Range<usize>, Arc<str>)>,
},
Noop,

View file

@ -26,7 +26,7 @@ use workspace::{
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
};
actions!(Deploy);
actions!(collab, [Deploy]);
pub fn init(cx: &mut AppContext) {
register_followable_item::<ChannelView>(cx)

File diff suppressed because it is too large Load diff

View file

@ -3,13 +3,14 @@ use client::UserId;
use collections::HashMap;
use editor::{AnchorRangeExt, Editor};
use gpui::{
elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
AnyView, AsyncWindowContext, FocusableView, Model, Render, SharedString, Task, View,
ViewContext, WeakView,
};
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
use lazy_static::lazy_static;
use project::search::SearchQuery;
use std::{sync::Arc, time::Duration};
use workspace::item::ItemHandle;
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
@ -19,8 +20,8 @@ lazy_static! {
}
pub struct MessageEditor {
pub editor: ViewHandle<Editor>,
channel_store: ModelHandle<ChannelStore>,
pub editor: View<Editor>,
channel_store: Model<ChannelStore>,
users: HashMap<String, UserId>,
mentions: Vec<UserId>,
mentions_task: Option<Task<()>>,
@ -30,8 +31,8 @@ pub struct MessageEditor {
impl MessageEditor {
pub fn new(
language_registry: Arc<LanguageRegistry>,
channel_store: ModelHandle<ChannelStore>,
editor: ViewHandle<Editor>,
channel_store: Model<ChannelStore>,
editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
editor.update(cx, |editor, cx| {
@ -48,13 +49,11 @@ impl MessageEditor {
cx.subscribe(&buffer, Self::on_buffer_event).detach();
let markdown = language_registry.language_for_name("Markdown");
cx.app_context()
.spawn(|mut cx| async move {
cx.spawn(|_, mut cx| async move {
let markdown = markdown.await?;
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx)
});
anyhow::Ok(())
})
})
.detach_and_log_err(cx);
@ -71,7 +70,7 @@ impl MessageEditor {
pub fn set_channel(
&mut self,
channel_id: u64,
channel_name: Option<String>,
channel_name: Option<SharedString>,
cx: &mut ViewContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
@ -132,26 +131,28 @@ impl MessageEditor {
fn on_buffer_event(
&mut self,
buffer: ModelHandle<Buffer>,
buffer: Model<Buffer>,
event: &language::Event,
cx: &mut ViewContext<Self>,
) {
if let language::Event::Reparsed | language::Event::Edited = event {
let buffer = buffer.read(cx).snapshot();
self.mentions_task = Some(cx.spawn(|this, cx| async move {
cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
cx.background_executor()
.timer(MENTIONS_DEBOUNCE_INTERVAL)
.await;
Self::find_mentions(this, buffer, cx).await;
}));
}
}
async fn find_mentions(
this: WeakViewHandle<MessageEditor>,
this: WeakView<MessageEditor>,
buffer: BufferSnapshot,
mut cx: AsyncAppContext,
mut cx: AsyncWindowContext,
) {
let (buffer, ranges) = cx
.background()
.background_executor()
.spawn(async move {
let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
(buffer, ranges)
@ -180,11 +181,7 @@ impl MessageEditor {
}
editor.clear_highlights::<Self>(cx);
editor.highlight_text::<Self>(
anchor_ranges,
theme::current(cx).chat_panel.rich_text.mention_highlight,
cx,
)
editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
});
this.mentions = mentioned_user_ids;
@ -192,21 +189,17 @@ impl MessageEditor {
})
.ok();
}
pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
self.editor.read(cx).focus_handle(cx)
}
}
impl Entity for MessageEditor {
type Event = ();
}
impl Render for MessageEditor {
type Element = AnyView;
impl View for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
ChildView::new(&self.editor, cx).into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.editor);
}
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
self.editor.to_any()
}
}
@ -214,7 +207,7 @@ impl View for MessageEditor {
mod tests {
use super::*;
use client::{Client, User, UserStore};
use gpui::{TestAppContext, WindowHandle};
use gpui::{Context as _, TestAppContext, VisualContext as _};
use language::{Language, LanguageConfig};
use rpc::proto;
use settings::SettingsStore;
@ -222,8 +215,17 @@ mod tests {
#[gpui::test]
async fn test_message_editor(cx: &mut TestAppContext) {
let editor = init_test(cx);
let editor = editor.root(cx);
let language_registry = init_test(cx);
let (editor, cx) = cx.add_window_view(|cx| {
MessageEditor::new(
language_registry,
ChannelStore::global(cx),
cx.build_view(|cx| Editor::auto_height(4, cx)),
cx,
)
});
cx.executor().run_until_parked();
editor.update(cx, |editor, cx| {
editor.set_members(
@ -255,7 +257,7 @@ mod tests {
});
});
cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
editor.update(cx, |editor, cx| {
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
@ -269,15 +271,14 @@ mod tests {
});
}
fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
cx.foreground().forbid_parking();
fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
cx.update(|cx| {
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
let settings = SettingsStore::test(cx);
cx.set_global(settings);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
editor::init(cx);
client::init(&client, cx);
@ -292,16 +293,6 @@ mod tests {
},
Some(tree_sitter_markdown::language()),
)));
let editor = cx.add_window(|cx| {
MessageEditor::new(
language_registry,
ChannelStore::global(cx),
cx.add_view(|cx| Editor::auto_height(4, None, cx)),
cx,
)
});
cx.foreground().run_until_parked();
editor
language_registry
}
}

View file

@ -92,7 +92,7 @@ use theme::{ActiveTheme, ThemeSettings};
// channel_id: ChannelId,
// }
#[derive(Action, PartialEq, Debug, Clone, Serialize, Deserialize)]
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub struct OpenChannelNotes {
pub channel_id: ChannelId,
}
@ -122,7 +122,11 @@ pub struct OpenChannelNotes {
// to: ChannelId,
// }
impl_actions!(collab_panel, [OpenChannelNotes]);
actions!(
collab_panel,
[
ToggleFocus,
Remove,
Secondary,
@ -131,6 +135,7 @@ actions!(
StartMoveChannel,
MoveSelected,
InsertSpace,
]
);
// impl_actions!(
@ -169,12 +174,12 @@ use editor::Editor;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, size, Action,
AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model,
MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce,
ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, ViewContext,
VisualContext, WeakView,
actions, canvas, div, img, impl_actions, overlay, point, prelude::*, px, rems, serde_json,
size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div,
EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement,
Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render,
RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View,
ViewContext, VisualContext, WeakView,
};
use project::{Fs, Project};
use serde_derive::{Deserialize, Serialize};
@ -192,6 +197,7 @@ use workspace::{
};
use crate::channel_view::ChannelView;
use crate::chat_panel::ChatPanel;
use crate::{face_pile::FacePile, CollaborationPanelSettings};
use self::channel_modal::ChannelModal;
@ -852,7 +858,7 @@ impl CollabPanel {
.extend(channel_store.ordered_channels().enumerate().map(
|(ix, (_, channel))| StringMatchCandidate {
id: ix,
string: channel.name.clone(),
string: channel.name.clone().into(),
char_bag: channel.name.chars().collect(),
},
));
@ -2102,14 +2108,13 @@ impl CollabPanel {
};
cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
todo!();
// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
// panel.update(cx, |panel, cx| {
// panel
// .select_channel(channel_id, None, cx)
// .detach_and_log_err(cx);
// });
// }
if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
panel.update(cx, |panel, cx| {
panel
.select_channel(channel_id, None, cx)
.detach_and_log_err(cx);
});
}
});
});
}
@ -2262,7 +2267,7 @@ impl CollabPanel {
}
};
Some(channel.name.as_str())
Some(channel.name.as_ref())
});
if let Some(name) = channel_name {
@ -2603,9 +2608,14 @@ impl CollabPanel {
Color::Default
} else {
Color::Muted
})
.on_click(cx.listener(move |this, _, cx| {
this.join_channel_chat(channel_id, cx)
}))
.tooltip(|cx| {
Tooltip::text("Open channel chat", cx)
}),
)
.tooltip(|cx| Tooltip::text("Open channel chat", cx)),
),
)
.child(
div()

View file

@ -13,12 +13,16 @@ use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::prelude::*;
use util::TryFutureExt;
use workspace::ModalView;
actions!(
channel_modal,
[
SelectNextControl,
ToggleMode,
ToggleMemberAdmin,
RemoveMember
]
);
// pub fn init(cx: &mut AppContext) {
@ -140,6 +144,7 @@ impl ChannelModal {
}
impl EventEmitter<DismissEvent> for ChannelModal {}
impl ModalView for ChannelModal {}
impl FocusableView for ChannelModal {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {

View file

@ -9,6 +9,7 @@ use std::sync::Arc;
use theme::ActiveTheme as _;
use ui::prelude::*;
use util::{ResultExt as _, TryFutureExt};
use workspace::ModalView;
pub fn init(cx: &mut AppContext) {
//Picker::<ContactFinderDelegate>::init(cx);
@ -95,6 +96,7 @@ pub struct ContactFinderDelegate {
}
impl EventEmitter<DismissEvent> for ContactFinder {}
impl ModalView for ContactFinder {}
impl FocusableView for ContactFinder {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@ use std::{rc::Rc, sync::Arc};
use call::{report_call_event_for_room, ActiveCall, Room};
pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
use gpui::{
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
WindowKind, WindowOptions,
@ -23,7 +24,10 @@ use settings::Settings;
use util::ResultExt;
use workspace::AppState;
actions!(ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall);
actions!(
collab,
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx);
@ -34,7 +38,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
collab_titlebar_item::init(cx);
collab_panel::init(cx);
channel_view::init(cx);
// chat_panel::init(cx);
chat_panel::init(cx);
notifications::init(&app_state, cx);
// cx.add_global_action(toggle_screen_sharing);
@ -157,6 +161,6 @@ fn notification_window_options(
// .into_any()
// }
// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
// cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
// }
fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
}

View file

@ -1,8 +1,9 @@
use gpui::{
div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext,
div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, RenderOnce, Styled,
WindowContext,
};
#[derive(Default)]
#[derive(Default, IntoElement)]
pub struct FacePile {
pub faces: Vec<AnyElement>,
}
@ -15,64 +16,15 @@ impl RenderOnce for FacePile {
let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
let isnt_last = ix < player_count - 1;
div().when(isnt_last, |div| div.neg_mr_1()).child(player)
div()
.z_index((player_count - ix) as u32)
.when(isnt_last, |div| div.neg_mr_1())
.child(player)
});
div().p_1().flex().items_center().children(player_list)
}
}
// impl Element for FacePile {
// type State = ();
// fn layout(
// &mut self,
// state: Option<Self::State>,
// cx: &mut WindowContext,
// ) -> (LayoutId, Self::State) {
// let mut width = 0.;
// let mut max_height = 0.;
// let mut faces = Vec::with_capacity(self.faces.len());
// for face in &mut self.faces {
// let layout = face.layout(cx);
// width += layout.x();
// max_height = f32::max(max_height, layout.y());
// faces.push(layout);
// }
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
// (cx.request_layout(&Style::default(), faces), ())
// // (
// // Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
// // (),
// // ))
// }
// fn paint(
// &mut self,
// bounds: RectF,
// visible_bounds: RectF,
// _layout: &mut Self::LayoutState,
// view: &mut V,
// cx: &mut ViewContext<V>,
// ) -> Self::PaintState {
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
// let origin_y = bounds.upper_right().y();
// let mut origin_x = bounds.upper_right().x();
// for face in self.faces.iter_mut().rev() {
// let size = face.size();
// origin_x -= size.x();
// let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
// cx.scene().push_layer(None);
// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
// cx.scene().pop_layer();
// origin_x += self.overlap;
// }
// ()
// }
// }
impl Extend<AnyElement> for FacePile {
fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
self.faces.extend(children);

View file

@ -3,9 +3,9 @@ use std::sync::Arc;
use workspace::AppState;
pub mod incoming_call_notification;
// pub mod project_shared_notification;
pub mod project_shared_notification;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
incoming_call_notification::init(app_state, cx);
//project_shared_notification::init(app_state, cx);
project_shared_notification::init(app_state, cx);
}

View file

@ -33,8 +33,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
height: px(64.),
};
for window in unique_screens {
let options = notification_window_options(window, window_size);
for screen in unique_screens {
let options = notification_window_options(screen, window_size);
let window = cx
.open_window(options, |cx| {
cx.build_view(|_| {
@ -47,15 +47,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
.unwrap();
notification_windows.push(window);
}
// for screen in cx.platform().screens() {
// let window = cx
// .add_window(notification_window_options(screen, window_size), |_| {
// IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
// });
// notification_windows.push(window);
// }
}
}
})
@ -84,21 +75,22 @@ impl IncomingCallNotificationState {
let active_call = ActiveCall::global(cx);
if accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.calling_user.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
let app_state = self.app_state.clone();
let cx: &mut AppContext = cx;
cx.spawn(|cx| async move {
join.await?;
if let Some(_project_id) = initial_project_id {
cx.update(|_cx| {
if let Some(_app_state) = app_state.upgrade() {
// workspace::join_remote_project(
// project_id,
// caller_user_id,
// app_state,
// cx,
// )
// .detach_and_log_err(cx);
if let Some(project_id) = initial_project_id {
cx.update(|cx| {
if let Some(app_state) = app_state.upgrade() {
workspace::join_remote_project(
project_id,
caller_user_id,
app_state,
cx,
)
.detach_and_log_err(cx);
}
})
.log_err();
@ -138,135 +130,25 @@ impl IncomingCallNotification {
)))
.child(self.render_buttons(cx)),
)
// let theme = &theme::current(cx).incoming_call_notification;
// let default_project = proto::ParticipantProject::default();
// let initial_project = self
// .call
// .initial_project
// .as_ref()
// .unwrap_or(&default_project);
// Flex::row()
// .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
// Image::from_data(avatar)
// .with_style(theme.caller_avatar)
// .aligned()
// }))
// .with_child(
// Flex::column()
// .with_child(
// Label::new(
// self.call.calling_user.github_login.clone(),
// theme.caller_username.text.clone(),
// )
// .contained()
// .with_style(theme.caller_username.container),
// )
// .with_child(
// Label::new(
// format!(
// "is sharing a project in Zed{}",
// if initial_project.worktree_root_names.is_empty() {
// ""
// } else {
// ":"
// }
// ),
// theme.caller_message.text.clone(),
// )
// .contained()
// .with_style(theme.caller_message.container),
// )
// .with_children(if initial_project.worktree_root_names.is_empty() {
// None
// } else {
// Some(
// Label::new(
// initial_project.worktree_root_names.join(", "),
// theme.worktree_roots.text.clone(),
// )
// .contained()
// .with_style(theme.worktree_roots.container),
// )
// })
// .contained()
// .with_style(theme.caller_metadata)
// .aligned(),
// )
// .contained()
// .with_style(theme.caller_container)
// .flex(1., true)
// .into_any()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
h_stack()
.child(
Button::new("accept", "Accept")
.render(cx)
// .bg(green())
.on_click({
.child(Button::new("accept", "Accept").render(cx).on_click({
let state = self.state.clone();
move |_, cx| state.respond(true, cx)
}),
)
.child(
Button::new("decline", "Decline")
.render(cx)
// .bg(red())
.on_click({
}))
.child(Button::new("decline", "Decline").render(cx).on_click({
let state = self.state.clone();
move |_, cx| state.respond(false, cx)
}),
)
// enum Accept {}
// enum Decline {}
// let theme = theme::current(cx);
// Flex::column()
// .with_child(
// MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
// let theme = &theme.incoming_call_notification;
// Label::new("Accept", theme.accept_button.text.clone())
// .aligned()
// .contained()
// .with_style(theme.accept_button.container)
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, |_, this, cx| {
// this.respond(true, cx);
// })
// .flex(1., true),
// )
// .with_child(
// MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
// let theme = &theme.incoming_call_notification;
// Label::new("Decline", theme.decline_button.text.clone())
// .aligned()
// .contained()
// .with_style(theme.decline_button.container)
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, |_, this, cx| {
// this.respond(false, cx);
// })
// .flex(1., true),
// )
// .constrained()
// .with_width(theme.incoming_call_notification.button_width)
// .into_any()
}))
}
}
impl Render for IncomingCallNotification {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().bg(red()).flex_none().child(self.render_caller(cx))
// Flex::row()
// .with_child()
// .with_child(self.render_buttons(cx))
// .contained()
// .with_background_color(background)
// .expanded()
// .into_any()
}
}

View file

@ -3,12 +3,11 @@ use call::{room, ActiveCall};
use client::User;
use collections::HashMap;
use gpui::{
elements::*,
geometry::vector::vec2f,
platform::{CursorStyle, MouseButton},
AppContext, Entity, View, ViewContext,
px, AppContext, Div, Element, ParentElement, Render, RenderOnce, Size, Styled, ViewContext,
VisualContext,
};
use std::sync::{Arc, Weak};
use ui::{h_stack, v_stack, Avatar, Button, Clickable, Label};
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@ -21,18 +20,22 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
project_id,
worktree_root_names,
} => {
let theme = &theme::current(cx).project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height);
let window_size = Size {
width: px(380.),
height: px(64.),
};
for screen in cx.platform().screens() {
let window =
cx.add_window(notification_window_options(screen, window_size), |_| {
for screen in cx.displays() {
let options = notification_window_options(screen, window_size);
let window = cx.open_window(options, |cx| {
cx.build_view(|_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
app_state.clone(),
)
})
});
notification_windows
.entry(*project_id)
@ -40,19 +43,31 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
.push(window);
}
}
room::Event::RemoteProjectUnshared { project_id }
| room::Event::RemoteProjectJoined { project_id }
| room::Event::RemoteProjectInvitationDiscarded { project_id } => {
if let Some(windows) = notification_windows.remove(&project_id) {
for window in windows {
window.remove(cx);
window
.update(cx, |_, cx| {
// todo!()
cx.remove_window();
})
.ok();
}
}
}
room::Event::Left => {
for (_, windows) in notification_windows.drain() {
for window in windows {
window.remove(cx);
window
.update(cx, |_, cx| {
// todo!()
cx.remove_window();
})
.ok();
}
}
}
@ -102,116 +117,60 @@ impl ProjectSharedNotification {
}
}
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).project_shared_notification;
Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| {
Image::from_data(avatar)
.with_style(theme.owner_avatar)
.aligned()
}))
.with_child(
Flex::column()
.with_child(
Label::new(
self.owner.github_login.clone(),
theme.owner_username.text.clone(),
fn render_owner(&self) -> impl Element {
h_stack()
.children(
self.owner
.avatar
.clone()
.map(|avatar| Avatar::data(avatar.clone())),
)
.contained()
.with_style(theme.owner_username.container),
)
.with_child(
Label::new(
format!(
.child(
v_stack()
.child(Label::new(self.owner.github_login.clone()))
.child(Label::new(format!(
"is sharing a project in Zed{}",
if self.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.message.text.clone(),
)
.contained()
.with_style(theme.message.container),
)
.with_children(if self.worktree_root_names.is_empty() {
)))
.children(if self.worktree_root_names.is_empty() {
None
} else {
Some(
Label::new(
self.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
Some(Label::new(self.worktree_root_names.join(", ")))
}),
)
.contained()
.with_style(theme.worktree_roots.container),
)
})
.contained()
.with_style(theme.owner_metadata)
.aligned(),
)
.contained()
.with_style(theme.owner_container)
.flex(1., true)
.into_any()
}
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum Open {}
enum Dismiss {}
let theme = theme::current(cx);
Flex::column()
.with_child(
MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
let theme = &theme.project_shared_notification;
Label::new("Open", theme.open_button.text.clone())
.aligned()
.contained()
.with_style(theme.open_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
.flex(1., true),
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
let this = cx.view().clone();
v_stack()
.child(Button::new("open", "Open").render(cx).on_click({
let this = this.clone();
move |_, cx| {
this.update(cx, |this, cx| this.join(cx));
}
}))
.child(
Button::new("dismiss", "Dismiss")
.render(cx)
.on_click(move |_, cx| {
this.update(cx, |this, cx| this.dismiss(cx));
}),
)
.with_child(
MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
let theme = &theme.project_shared_notification;
Label::new("Dismiss", theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.dismiss(cx);
})
.flex(1., true),
)
.constrained()
.with_width(theme.project_shared_notification.button_width)
.into_any()
}
}
impl Entity for ProjectSharedNotification {
type Event = ();
}
impl Render for ProjectSharedNotification {
type Element = Div;
impl View for ProjectSharedNotification {
fn ui_name() -> &'static str {
"ProjectSharedNotification"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
let background = theme::current(cx).project_shared_notification.background;
Flex::row()
.with_child(self.render_owner(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.into_any()
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
h_stack()
.size_full()
.bg(gpui::red())
.child(self.render_owner())
.child(self.render_buttons(cx))
}
}

View file

@ -7,7 +7,7 @@ use collections::{CommandPaletteFilter, HashMap};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
@ -16,16 +16,18 @@ use util::{
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
ResultExt,
};
use workspace::Workspace;
use workspace::{ModalView, Workspace};
use zed_actions::OpenZedURL;
actions!(Toggle);
actions!(command_palette, [Toggle]);
pub fn init(cx: &mut AppContext) {
cx.set_global(HitCounts::default());
cx.observe_new_views(CommandPalette::register).detach();
}
impl ModalView for CommandPalette {}
pub struct CommandPalette {
picker: View<Picker<CommandPaletteDelegate>>,
}
@ -47,7 +49,7 @@ impl CommandPalette {
.available_actions()
.into_iter()
.filter_map(|action| {
let name = gpui::remove_the_2(action.name());
let name = action.name();
let namespace = name.split("::").next().unwrap_or("malformed action name");
if filter.is_some_and(|f| {
f.hidden_namespaces.contains(namespace)
@ -59,7 +61,6 @@ impl CommandPalette {
Some(Command {
name: humanize_action_name(&name),
action,
keystrokes: vec![], // todo!()
})
})
.collect();
@ -108,7 +109,6 @@ pub struct CommandPaletteDelegate {
struct Command {
name: String,
action: Box<dyn Action>,
keystrokes: Vec<Keystroke>,
}
impl Clone for Command {
@ -116,7 +116,6 @@ impl Clone for Command {
Self {
name: self.name.clone(),
action: self.action.boxed_clone(),
keystrokes: self.keystrokes.clone(),
}
}
}
@ -227,6 +226,7 @@ impl PickerDelegate for CommandPaletteDelegate {
})
}
}
if let Some(CommandInterceptResult {
action,
string,
@ -242,7 +242,6 @@ impl PickerDelegate for CommandPaletteDelegate {
commands.push(Command {
name: string.clone(),
action,
keystrokes: vec![],
});
matches.insert(
0,
@ -254,6 +253,7 @@ impl PickerDelegate for CommandPaletteDelegate {
},
)
}
picker
.update(&mut cx, |picker, _| {
let delegate = &mut picker.delegate;
@ -283,6 +283,8 @@ impl PickerDelegate for CommandPaletteDelegate {
}
let action_ix = self.matches[self.selected_ix].candidate_id;
let command = self.commands.swap_remove(action_ix);
self.matches.clear();
self.commands.clear();
cx.update_global(|hit_counts: &mut HitCounts, _| {
*hit_counts.0.entry(command.name).or_default() += 1;
});
@ -298,13 +300,8 @@ impl PickerDelegate for CommandPaletteDelegate {
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let Some(r#match) = self.matches.get(ix) else {
return None;
};
let Some(command) = self.commands.get(r#match.candidate_id) else {
return None;
};
let r#match = self.matches.get(ix)?;
let command = self.commands.get(r#match.candidate_id)?;
Some(
ListItem::new(ix).inset(true).selected(selected).child(
h_stack()
@ -352,8 +349,7 @@ impl std::fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Command")
.field("name", &self.name)
.field("keystrokes", &self.keystrokes)
.finish()
.finish_non_exhaustive()
}
}

View file

@ -34,12 +34,15 @@ use util::{
};
actions!(
copilot,
[
Suggest,
NextSuggestion,
PreviousSuggestion,
Reinstall,
SignIn,
SignOut
]
);
pub fn init(

View file

@ -43,7 +43,7 @@ use workspace::{
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
};
actions!(Deploy, ToggleWarnings);
actions!(diagnostics, [Deploy, ToggleWarnings]);
const CONTEXT_LINE_COUNT: u32 = 1;

View file

@ -12,6 +12,7 @@ mod link_go_to_definition;
mod mouse_context_menu;
pub mod movement;
mod persistence;
mod rust_analyzer_ext;
pub mod scroll;
pub mod selections_collection;
@ -300,6 +301,7 @@ actions!(
DeleteToEndOfLine,
CutToEndOfLine,
DuplicateLine,
ExpandMacroRecursively,
MoveLineUp,
MoveLineDown,
JoinLines,
@ -425,6 +427,8 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(cx: &mut AppContext) {
init_settings(cx);
rust_analyzer_ext::apply_related_actions(cx);
cx.add_action(Editor::new_file);
cx.add_action(Editor::new_file_in_direction);
cx.add_action(Editor::cancel);

View file

@ -0,0 +1,98 @@
use std::sync::Arc;
use anyhow::Context as _;
use gpui::{AppContext, Task, ViewContext};
use language::Language;
use multi_buffer::MultiBuffer;
use project::lsp_ext_command::ExpandMacro;
use text::ToPointUtf16;
use crate::{Editor, ExpandMacroRecursively};
pub fn apply_related_actions(cx: &mut AppContext) {
cx.add_async_action(expand_macro_recursively);
}
pub fn expand_macro_recursively(
editor: &mut Editor,
_: &ExpandMacroRecursively,
cx: &mut ViewContext<'_, '_, Editor>,
) -> Option<Task<anyhow::Result<()>>> {
if editor.selections.count() == 0 {
return None;
}
let project = editor.project.as_ref()?;
let workspace = editor.workspace(cx)?;
let multibuffer = editor.buffer().read(cx);
let (trigger_anchor, rust_language, server_to_query, buffer) = editor
.selections
.disjoint_anchors()
.into_iter()
.filter(|selection| selection.start == selection.end)
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
.filter_map(|(buffer_id, trigger_anchor)| {
let buffer = multibuffer.buffer(buffer_id)?;
let rust_language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
if !is_rust_language(&rust_language) {
return None;
}
Some((trigger_anchor, rust_language, buffer))
})
.find_map(|(trigger_anchor, rust_language, buffer)| {
project
.read(cx)
.language_servers_for_buffer(buffer.read(cx), cx)
.into_iter()
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == "rust-analyzer" {
Some((
trigger_anchor,
Arc::clone(&rust_language),
server.server_id(),
buffer.clone(),
))
} else {
None
}
})
})?;
let project = project.clone();
let buffer_snapshot = buffer.read(cx).snapshot();
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
let expand_macro_task = project.update(cx, |project, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
ExpandMacro { position },
cx,
)
});
Some(cx.spawn(|_, mut cx| async move {
let macro_expansion = expand_macro_task.await.context("expand macro")?;
if macro_expansion.is_empty() {
log::info!("Empty macro expansion for position {position:?}");
return Ok(());
}
let buffer = project.update(&mut cx, |project, cx| {
project.create_buffer(&macro_expansion.expansion, Some(rust_language), cx)
})?;
workspace.update(&mut cx, |workspace, cx| {
let buffer = cx.add_model(|cx| {
MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name)
});
workspace.add_item(
Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
cx,
);
});
anyhow::Ok(())
}))
}
fn is_rust_language(language: &Language) -> bool {
language.name().as_ref() == "Rust"
}

View file

@ -13,6 +13,7 @@ mod link_go_to_definition;
mod mouse_context_menu;
pub mod movement;
mod persistence;
mod rust_analyzer_ext;
pub mod scroll;
pub mod selections_collection;
@ -39,8 +40,8 @@ use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate};
use git::diff_hunk_to_display;
use gpui::{
actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
actions, div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action,
AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
DispatchPhase, Div, ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures,
FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model,
MouseButton, ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText,
@ -107,7 +108,7 @@ use ui::{
use ui::{prelude::*, IconSize};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{
item::{ItemEvent, ItemHandle},
item::{Item, ItemEvent, ItemHandle},
searchable::SearchEvent,
ItemNavHistory, Pane, SplitDirection, ViewId, Workspace,
};
@ -185,82 +186,101 @@ pub fn render_parsed_markdown(
})
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectNext {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectPrevious {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectAllMatches {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectToBeginningOfLine {
#[serde(default)]
stop_at_soft_wraps: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MovePageUp {
#[serde(default)]
center_cursor: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MovePageDown {
#[serde(default)]
center_cursor: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectToEndOfLine {
#[serde(default)]
stop_at_soft_wraps: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleCodeActions {
#[serde(default)]
pub deployed_from_indicator: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCompletion {
#[serde(default)]
pub item_ix: Option<usize>,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCodeAction {
#[serde(default)]
pub item_ix: Option<usize>,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleComments {
#[serde(default)]
pub advance_downwards: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct FoldAt {
pub buffer_row: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct UnfoldAt {
pub buffer_row: u32,
}
impl_actions!(
editor,
[
SelectNext,
SelectPrevious,
SelectAllMatches,
SelectToBeginningOfLine,
MovePageUp,
MovePageDown,
SelectToEndOfLine,
ToggleCodeActions,
ConfirmCompletion,
ConfirmCodeAction,
ToggleComments,
FoldAt,
UnfoldAt
]
);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InlayId {
Suggestion(usize),
@ -277,6 +297,8 @@ impl InlayId {
}
actions!(
editor,
[
AddSelectionAbove,
AddSelectionBelow,
Backspace,
@ -308,6 +330,7 @@ actions!(
DeleteToPreviousSubwordStart,
DeleteToPreviousWordStart,
DuplicateLine,
ExpandMacroRecursively,
FindAllReferences,
Fold,
FoldSelectedRanges,
@ -392,6 +415,7 @@ actions!(
Undo,
UndoSelection,
UnfoldLines,
]
);
enum DocumentHighlightRead {}
@ -9319,7 +9343,6 @@ impl Render for Editor {
scrollbar_width: px(12.),
syntax: cx.theme().syntax().clone(),
diagnostic_style: cx.theme().diagnostic_style(),
// TODO kb find `HighlightStyle` usages
// todo!("what about the rest of the highlight style parts?")
inlays_style: HighlightStyle {
color: Some(cx.theme().status().hint),

View file

@ -32,7 +32,7 @@ use gpui::{
Style, Styled, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
};
use itertools::Itertools;
use language::language_settings::ShowWhitespaceSetting;
use language::{language_settings::ShowWhitespaceSetting, Language};
use multi_buffer::Anchor;
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
@ -135,11 +135,13 @@ impl EditorElement {
fn register_actions(&self, cx: &mut WindowContext) {
let view = &self.editor;
self.editor.update(cx, |editor, cx| {
view.update(cx, |editor, cx| {
for action in editor.editor_actions.iter() {
(action)(cx)
}
});
crate::rust_analyzer_ext::apply_related_actions(view, cx);
register_action(view, cx, Editor::move_left);
register_action(view, cx, Editor::move_right);
register_action(view, cx, Editor::move_down);
@ -385,17 +387,17 @@ impl EditorElement {
gutter_bounds: Bounds<Pixels>,
stacking_order: &StackingOrder,
cx: &mut ViewContext<Editor>,
) -> bool {
) {
let mut click_count = event.click_count;
let modifiers = event.modifiers;
if gutter_bounds.contains_point(&event.position) {
click_count = 3; // Simulate triple-click when clicking the gutter to select lines
} else if !text_bounds.contains_point(&event.position) {
return false;
return;
}
if !cx.was_top_layer(&event.position, stacking_order) {
return false;
return;
}
let point_for_position = position_map.point_for_position(text_bounds, event.position);
@ -427,7 +429,7 @@ impl EditorElement {
);
}
true
cx.stop_propagation();
}
fn mouse_right_down(
@ -436,9 +438,9 @@ impl EditorElement {
position_map: &PositionMap,
text_bounds: Bounds<Pixels>,
cx: &mut ViewContext<Editor>,
) -> bool {
) {
if !text_bounds.contains_point(&event.position) {
return false;
return;
}
let point_for_position = position_map.point_for_position(text_bounds, event.position);
mouse_context_menu::deploy_context_menu(
@ -447,7 +449,7 @@ impl EditorElement {
point_for_position.previous_valid,
cx,
);
true
cx.stop_propagation();
}
fn mouse_up(
@ -457,7 +459,7 @@ impl EditorElement {
text_bounds: Bounds<Pixels>,
stacking_order: &StackingOrder,
cx: &mut ViewContext<Editor>,
) -> bool {
) {
let end_selection = editor.has_pending_selection();
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
@ -479,10 +481,10 @@ impl EditorElement {
go_to_fetched_definition(editor, point, split, cx);
}
return true;
cx.stop_propagation();
} else if end_selection {
cx.stop_propagation();
}
end_selection
}
fn mouse_moved(
@ -493,7 +495,7 @@ impl EditorElement {
gutter_bounds: Bounds<Pixels>,
stacking_order: &StackingOrder,
cx: &mut ViewContext<Editor>,
) -> bool {
) {
let modifiers = event.modifiers;
if editor.has_pending_selection() && event.pressed_button == Some(MouseButton::Left) {
let point_for_position = position_map.point_for_position(text_bounds, event.position);
@ -562,11 +564,13 @@ impl EditorElement {
}
}
true
cx.stop_propagation();
} else {
update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx);
hover_at(editor, None, cx);
gutter_hovered && was_top
if gutter_hovered && was_top {
cx.stop_propagation();
}
}
}
@ -576,9 +580,9 @@ impl EditorElement {
position_map: &PositionMap,
bounds: &InteractiveBounds,
cx: &mut ViewContext<Editor>,
) -> bool {
) {
if !bounds.visibly_contains(&event.position, cx) {
return false;
return;
}
let line_height = position_map.line_height;
@ -602,8 +606,7 @@ impl EditorElement {
let y = f32::from((scroll_position.y * line_height - delta.y) / line_height);
let scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
editor.scroll(scroll_position, axis, cx);
true
cx.stop_propagation();
}
fn paint_background(
@ -749,6 +752,7 @@ impl EditorElement {
}
}
cx.with_z_index(1, |cx| {
for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() {
if let Some(mut fold_indicator) = fold_indicator {
let mut fold_indicator = fold_indicator.into_any_element();
@ -763,7 +767,8 @@ impl EditorElement {
ix as f32 * line_height - (scroll_top % line_height),
);
let centering_offset = point(
(layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width) / 2.,
(layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width)
/ 2.,
(line_height - fold_indicator_size.height) / 2.,
);
let origin = bounds.origin + position + centering_offset;
@ -787,6 +792,7 @@ impl EditorElement {
button.draw(bounds.origin + point(x, y), available_space, cx);
}
});
}
fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut WindowContext) {
@ -824,8 +830,8 @@ impl EditorElement {
};
let color = match status {
DiffHunkStatus::Added => gpui::green(), // todo!("use the appropriate color")
DiffHunkStatus::Modified => gpui::yellow(), // todo!("use the appropriate color")
DiffHunkStatus::Added => cx.theme().status().created,
DiffHunkStatus::Modified => cx.theme().status().modified,
//TODO: This rendering is entirely a horrible hack
DiffHunkStatus::Removed => {
@ -842,7 +848,7 @@ impl EditorElement {
cx.paint_quad(
highlight_bounds,
Corners::all(1. * line_height),
gpui::red(), // todo!("use the right color")
cx.theme().status().deleted,
Edges::default(),
transparent_black(),
);
@ -1230,203 +1236,216 @@ impl EditorElement {
bounds.upper_right().x - self.style.scrollbar_width
}
// fn paint_scrollbar(
// &mut self,
// bounds: Bounds<Pixels>,
// layout: &mut LayoutState,
// editor: &Editor,
// cx: &mut ViewContext<Editor>,
// ) {
// enum ScrollbarMouseHandlers {}
// if layout.mode != EditorMode::Full {
// return;
// }
fn paint_scrollbar(
&mut self,
bounds: Bounds<Pixels>,
layout: &mut LayoutState,
cx: &mut WindowContext,
) {
if layout.mode != EditorMode::Full {
return;
}
// let style = &self.style.theme.scrollbar;
let top = bounds.origin.y;
let bottom = bounds.lower_left().y;
let right = bounds.lower_right().x;
let left = self.scrollbar_left(&bounds);
let row_range = layout.scrollbar_row_range.clone();
let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
// let top = bounds.min_y;
// let bottom = bounds.max_y;
// let right = bounds.max_x;
// let left = self.scrollbar_left(&bounds);
// let row_range = &layout.scrollbar_row_range;
// let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
let mut height = bounds.size.height;
let mut first_row_y_offset = px(0.0);
// let mut height = bounds.height();
// let mut first_row_y_offset = 0.0;
// Impose a minimum height on the scrollbar thumb
let row_height = height / max_row;
let min_thumb_height = layout.position_map.line_height;
let thumb_height = (row_range.end - row_range.start) * row_height;
if thumb_height < min_thumb_height {
first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
height -= min_thumb_height - thumb_height;
}
// // Impose a minimum height on the scrollbar thumb
// let row_height = height / max_row;
// let min_thumb_height =
// style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
// let thumb_height = (row_range.end - row_range.start) * row_height;
// if thumb_height < min_thumb_height {
// first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
// height -= min_thumb_height - thumb_height;
// }
let y_for_row = |row: f32| -> Pixels { top + first_row_y_offset + row * row_height };
// let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * row_height };
let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
let track_bounds = Bounds::from_corners(point(left, top), point(right, bottom));
let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom));
// let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
// let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
// let track_bounds = Bounds::<Pixels>::from_points(point(left, top), point(right, bottom));
// let thumb_bounds = Bounds::<Pixels>::from_points(point(left, thumb_top), point(right, thumb_bottom));
if layout.show_scrollbars {
cx.paint_quad(
track_bounds,
Corners::default(),
gpui::blue(), // todo!("style.track.background_color")
Edges::default(), // todo!("style.track.border")
transparent_black(), // todo!("style.track.border")
);
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
if layout.is_singleton && scrollbar_settings.selections {
let start_anchor = Anchor::min();
let end_anchor = Anchor::max();
let background_ranges = self
.editor
.read(cx)
.background_highlight_row_ranges::<crate::items::BufferSearchHighlights>(
start_anchor..end_anchor,
&layout.position_map.snapshot,
50000,
);
for range in background_ranges {
let start_y = y_for_row(range.start().row() as f32);
let mut end_y = y_for_row(range.end().row() as f32);
if end_y - start_y < px(1.) {
end_y = start_y + px(1.);
}
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
cx.paint_quad(
bounds,
Corners::default(),
gpui::yellow(), // todo!("theme.editor.scrollbar")
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
gpui::green(), // todo!("style.thumb.border.color")
);
}
}
// if layout.show_scrollbars {
// cx.paint_quad(Quad {
// bounds: track_bounds,
// border: style.track.border.into(),
// background: style.track.background_color,
// ..Default::default()
// });
// let scrollbar_settings = settings::get::<EditorSettings>(cx).scrollbar;
// let theme = theme::current(cx);
// let scrollbar_theme = &theme.editor.scrollbar;
// if layout.is_singleton && scrollbar_settings.selections {
// let start_anchor = Anchor::min();
// let end_anchor = Anchor::max;
// let color = scrollbar_theme.selections;
// let border = Border {
// width: 1.,
// color: style.thumb.border.color,
// overlay: false,
// top: false,
// right: true,
// bottom: false,
// left: true,
// };
// let mut push_region = |start: DisplayPoint, end: DisplayPoint| {
// let start_y = y_for_row(start.row() as f32);
// let mut end_y = y_for_row(end.row() as f32);
// if end_y - start_y < 1. {
// end_y = start_y + 1.;
// }
// let bounds = Bounds::<Pixels>::from_points(point(left, start_y), point(right, end_y));
if layout.is_singleton && scrollbar_settings.git_diff {
for hunk in layout
.position_map
.snapshot
.buffer_snapshot
.git_diff_hunks_in_range(0..(max_row.floor() as u32))
{
let start_display = Point::new(hunk.buffer_range.start, 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let end_display = Point::new(hunk.buffer_range.end, 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let start_y = y_for_row(start_display.row() as f32);
let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
y_for_row((end_display.row() + 1) as f32)
} else {
y_for_row((end_display.row()) as f32)
};
// cx.paint_quad(Quad {
// bounds,
// background: Some(color),
// border: border.into(),
// corner_radii: style.thumb.corner_radii.into(),
// })
// };
// let background_ranges = editor
// .background_highlight_row_ranges::<crate::items::BufferSearchHighlights>(
// start_anchor..end_anchor,
// &layout.position_map.snapshot,
// 50000,
// );
// for row in background_ranges {
// let start = row.start();
// let end = row.end();
// push_region(*start, *end);
// }
// }
if end_y - start_y < px(1.) {
end_y = start_y + px(1.);
}
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
// if layout.is_singleton && scrollbar_settings.git_diff {
// let diff_style = scrollbar_theme.git.clone();
// for hunk in layout
// .position_map
// .snapshot
// .buffer_snapshot
// .git_diff_hunks_in_range(0..(max_row.floor() as u32))
// {
// let start_display = Point::new(hunk.buffer_range.start, 0)
// .to_display_point(&layout.position_map.snapshot.display_snapshot);
// let end_display = Point::new(hunk.buffer_range.end, 0)
// .to_display_point(&layout.position_map.snapshot.display_snapshot);
// let start_y = y_for_row(start_display.row() as f32);
// let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
// y_for_row((end_display.row() + 1) as f32)
// } else {
// y_for_row((end_display.row()) as f32)
// };
let color = match hunk.status() {
DiffHunkStatus::Added => gpui::green(), // todo!("use the right color")
DiffHunkStatus::Modified => gpui::yellow(), // todo!("use the right color")
DiffHunkStatus::Removed => gpui::red(), // todo!("use the right color")
};
cx.paint_quad(
bounds,
Corners::default(),
color,
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
gpui::green(), // todo!("style.thumb.border.color")
);
}
}
// if end_y - start_y < 1. {
// end_y = start_y + 1.;
// }
// let bounds = Bounds::<Pixels>::from_points(point(left, start_y), point(right, end_y));
cx.paint_quad(
thumb_bounds,
Corners::default(),
gpui::black(), // todo!("style.thumb.background_color")
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
gpui::green(), // todo!("style.thumb.border.color")
);
}
// let color = match hunk.status() {
// DiffHunkStatus::Added => diff_style.inserted,
// DiffHunkStatus::Modified => diff_style.modified,
// DiffHunkStatus::Removed => diff_style.deleted,
// };
let mouse_position = cx.mouse_position();
if track_bounds.contains_point(&mouse_position) {
cx.set_cursor_style(CursorStyle::Arrow);
}
// let border = Border {
// width: 1.,
// color: style.thumb.border.color,
// overlay: false,
// top: false,
// right: true,
// bottom: false,
// left: true,
// };
cx.on_mouse_event({
let editor = self.editor.clone();
move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Capture {
return;
}
// cx.paint_quad(Quad {
// bounds,
// background: Some(color),
// border: border.into(),
// corner_radii: style.thumb.corner_radii.into(),
// })
// }
// }
editor.update(cx, |editor, cx| {
if event.pressed_button == Some(MouseButton::Left)
&& editor.scroll_manager.is_dragging_scrollbar()
{
let y = mouse_position.y;
let new_y = event.position.y;
if thumb_top < y && y < thumb_bottom {
let mut position = editor.scroll_position(cx);
position.y += (new_y - y) * (max_row as f32) / height;
if position.y < 0.0 {
position.y = 0.0;
}
editor.set_scroll_position(position, cx);
}
cx.stop_propagation();
} else {
editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
if track_bounds.contains_point(&event.position) {
editor.scroll_manager.show_scrollbar(cx);
}
}
})
}
});
// cx.paint_quad(Quad {
// bounds: thumb_bounds,
// border: style.thumb.border.into(),
// background: style.thumb.background_color,
// corner_radii: style.thumb.corner_radii.into(),
// });
// }
if self.editor.read(cx).scroll_manager.is_dragging_scrollbar() {
cx.on_mouse_event({
let editor = self.editor.clone();
move |event: &MouseUpEvent, phase, cx| {
editor.update(cx, |editor, cx| {
editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
cx.stop_propagation();
});
}
});
} else {
cx.on_mouse_event({
let editor = self.editor.clone();
move |event: &MouseDownEvent, phase, cx| {
editor.update(cx, |editor, cx| {
if track_bounds.contains_point(&event.position) {
editor.scroll_manager.set_is_dragging_scrollbar(true, cx);
// cx.scene().push_cursor_region(CursorRegion {
// bounds: track_bounds,
// style: CursorStyle::Arrow,
// });
// let region_id = cx.view_id();
// cx.scene().push_mouse_region(
// MouseRegion::new::<ScrollbarMouseHandlers>(region_id, region_id, track_bounds)
// .on_move(move |event, editor: &mut Editor, cx| {
// if event.pressed_button.is_none() {
// editor.scroll_manager.show_scrollbar(cx);
// }
// })
// .on_down(MouseButton::Left, {
// let row_range = row_range.clone();
// move |event, editor: &mut Editor, cx| {
// let y = event.position.y;
// if y < thumb_top || thumb_bottom < y {
// let center_row = ((y - top) * max_row as f32 / height).round() as u32;
// let top_row = center_row
// .saturating_sub((row_range.end - row_range.start) as u32 / 2);
// let mut position = editor.scroll_position(cx);
// position.set_y(top_row as f32);
// editor.set_scroll_position(position, cx);
// } else {
// editor.scroll_manager.show_scrollbar(cx);
// }
// }
// })
// .on_drag(MouseButton::Left, {
// move |event, editor: &mut Editor, cx| {
// if event.end {
// return;
// }
let y = event.position.y;
if y < thumb_top || thumb_bottom < y {
let center_row =
((y - top) * max_row as f32 / height).round() as u32;
let top_row = center_row
.saturating_sub((row_range.end - row_range.start) as u32 / 2);
let mut position = editor.scroll_position(cx);
position.y = top_row as f32;
editor.set_scroll_position(position, cx);
} else {
editor.scroll_manager.show_scrollbar(cx);
}
// let y = event.prev_mouse_position.y;
// let new_y = event.position.y;
// if thumb_top < y && y < thumb_bottom {
// let mut position = editor.scroll_position(cx);
// position.set_y(position.y + (new_y - y) * (max_row as f32) / height);
// if position.y < 0.0 {
// position.set_y(0.);
// }
// editor.set_scroll_position(position, cx);
// }
// }
// }),
// );
// }
cx.stop_propagation();
}
});
}
});
}
}
#[allow(clippy::too_many_arguments)]
fn paint_highlighted_range(
@ -2452,12 +2471,9 @@ impl EditorElement {
return;
}
let handled = editor.update(cx, |editor, cx| {
editor.update(cx, |editor, cx| {
Self::scroll(editor, event, &position_map, &interactive_bounds, cx)
});
if handled {
cx.stop_propagation();
}
}
});
@ -2471,7 +2487,7 @@ impl EditorElement {
return;
}
let handled = match event.button {
match event.button {
MouseButton::Left => editor.update(cx, |editor, cx| {
Self::mouse_left_down(
editor,
@ -2486,12 +2502,8 @@ impl EditorElement {
MouseButton::Right => editor.update(cx, |editor, cx| {
Self::mouse_right_down(editor, event, &position_map, text_bounds, cx)
}),
_ => false,
_ => {}
};
if handled {
cx.stop_propagation()
}
}
});
@ -2501,7 +2513,7 @@ impl EditorElement {
let stacking_order = cx.stacking_order().clone();
move |event: &MouseUpEvent, phase, cx| {
let handled = editor.update(cx, |editor, cx| {
editor.update(cx, |editor, cx| {
Self::mouse_up(
editor,
event,
@ -2511,10 +2523,6 @@ impl EditorElement {
cx,
)
});
if handled {
cx.stop_propagation()
}
}
});
cx.on_mouse_event({
@ -2527,7 +2535,7 @@ impl EditorElement {
return;
}
let stop_propogating = editor.update(cx, |editor, cx| {
editor.update(cx, |editor, cx| {
Self::mouse_moved(
editor,
event,
@ -2538,10 +2546,6 @@ impl EditorElement {
cx,
)
});
if stop_propogating {
cx.stop_propagation()
}
}
});
}
@ -2840,9 +2844,11 @@ impl Element for EditorElement {
cx.with_z_index(1, |cx| {
cx.with_element_id(Some("editor_blocks"), |cx| {
self.paint_blocks(bounds, &mut layout, cx);
})
});
})
}
cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx));
});
});
})
@ -2944,7 +2950,7 @@ impl PositionMap {
) -> PointForPosition {
let scroll_position = self.snapshot.scroll_position();
let position = position - text_bounds.origin;
let y = position.y.max(px(0.)).min(self.size.width);
let y = position.y.max(px(0.)).min(self.size.height);
let x = position.x + (scroll_position.x * self.em_width);
let row = (f32::from(y / self.line_height) + scroll_position.y) as u32;

View file

@ -26,7 +26,7 @@ pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
pub const HOVER_POPOVER_GAP: Pixels = px(10.);
actions!(Hover);
actions!(editor, [Hover]);
/// Bindable action which uses the most recent selection head to trigger a hover
pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {

View file

@ -0,0 +1,119 @@
use std::sync::Arc;
use anyhow::Context as _;
use gpui::{Context, Model, View, ViewContext, VisualContext, WindowContext};
use language::Language;
use multi_buffer::MultiBuffer;
use project::lsp_ext_command::ExpandMacro;
use text::ToPointUtf16;
use crate::{element::register_action, Editor, ExpandMacroRecursively};
pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
let is_rust_related = editor.update(cx, |editor, cx| {
editor
.buffer()
.read(cx)
.all_buffers()
.iter()
.any(|b| match b.read(cx).language() {
Some(l) => is_rust_language(l),
None => false,
})
});
if is_rust_related {
register_action(editor, cx, expand_macro_recursively);
}
}
pub fn expand_macro_recursively(
editor: &mut Editor,
_: &ExpandMacroRecursively,
cx: &mut ViewContext<'_, Editor>,
) {
if editor.selections.count() == 0 {
return;
}
let Some(project) = &editor.project else {
return;
};
let Some(workspace) = editor.workspace() else {
return;
};
let multibuffer = editor.buffer().read(cx);
let Some((trigger_anchor, rust_language, server_to_query, buffer)) = editor
.selections
.disjoint_anchors()
.into_iter()
.filter(|selection| selection.start == selection.end)
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
.filter_map(|(buffer_id, trigger_anchor)| {
let buffer = multibuffer.buffer(buffer_id)?;
let rust_language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
if !is_rust_language(&rust_language) {
return None;
}
Some((trigger_anchor, rust_language, buffer))
})
.find_map(|(trigger_anchor, rust_language, buffer)| {
project
.read(cx)
.language_servers_for_buffer(buffer.read(cx), cx)
.into_iter()
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == "rust-analyzer" {
Some((
trigger_anchor,
Arc::clone(&rust_language),
server.server_id(),
buffer.clone(),
))
} else {
None
}
})
})
else {
return;
};
let project = project.clone();
let buffer_snapshot = buffer.read(cx).snapshot();
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
let expand_macro_task = project.update(cx, |project, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
ExpandMacro { position },
cx,
)
});
cx.spawn(|editor, mut cx| async move {
let macro_expansion = expand_macro_task.await.context("expand macro")?;
if macro_expansion.is_empty() {
log::info!("Empty macro expansion for position {position:?}");
return Ok(());
}
let buffer = project.update(&mut cx, |project, cx| {
project.create_buffer(&macro_expansion.expansion, Some(rust_language), cx)
})??;
workspace.update(&mut cx, |workspace, cx| {
let buffer = cx.build_model(|cx| {
MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name)
});
workspace.add_item(
Box::new(cx.build_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
cx,
);
})
})
.detach_and_log_err(cx);
}
fn is_rust_language(language: &Language) -> bool {
language.name().as_ref() == "Rust"
}

View file

@ -136,6 +136,7 @@ pub struct ScrollManager {
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>,
dragging_scrollbar: bool,
visible_line_count: Option<f32>,
}
@ -148,6 +149,7 @@ impl ScrollManager {
autoscroll_request: None,
show_scrollbars: true,
hide_scrollbar_task: None,
dragging_scrollbar: false,
last_autoscroll: None,
visible_line_count: None,
}
@ -278,6 +280,17 @@ impl ScrollManager {
self.autoscroll_request.is_some()
}
pub fn is_dragging_scrollbar(&self) -> bool {
self.dragging_scrollbar
}
pub fn set_is_dragging_scrollbar(&mut self, dragging: bool, cx: &mut ViewContext<Editor>) {
if dragging != self.dragging_scrollbar {
self.dragging_scrollbar = dragging;
cx.notify();
}
}
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
if max < self.anchor.offset.x {
self.anchor.offset.x = max;

View file

@ -32,7 +32,7 @@ impl Render for DeployFeedbackButton {
IconButton::new("give-feedback", Icon::Envelope)
.style(ui::ButtonStyle::Subtle)
.selected(is_open)
.tooltip(|cx| Tooltip::text("Give Feedback", cx))
.tooltip(|cx| Tooltip::text("Share Feedback", cx))
.on_click(|_, cx| {
cx.dispatch_action(Box::new(GiveFeedback));
})

View file

@ -5,15 +5,18 @@ use workspace::Workspace;
pub mod deploy_feedback_button;
pub mod feedback_modal;
actions!(GiveFeedback, SubmitFeedback);
actions!(feedback, [GiveFeedback, SubmitFeedback]);
mod system_specs;
actions!(
zed,
[
CopySystemSpecsIntoClipboard,
FileBugReport,
RequestFeature,
OpenZedCommunityRepo
]
);
pub fn init(cx: &mut AppContext) {

View file

@ -1,28 +1,38 @@
use std::{ops::RangeInclusive, sync::Arc};
use anyhow::bail;
use anyhow::{anyhow, bail};
use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorEvent};
use futures::AsyncReadExt;
use gpui::{
div, red, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
FocusableView, Model, PromptLevel, Render, Task, View, ViewContext,
div, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
Model, PromptLevel, Render, Task, View, ViewContext,
};
use isahc::Request;
use language::Buffer;
use project::Project;
use regex::Regex;
use serde_derive::Serialize;
use ui::{prelude::*, Button, ButtonStyle, Label, Tooltip};
use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
use util::ResultExt;
use workspace::Workspace;
use workspace::{ModalView, Workspace};
use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo};
// For UI testing purposes
const SEND_SUCCESS_IN_DEV_MODE: bool = true;
// Temporary, until tests are in place
#[cfg(debug_assertions)]
const DEV_MODE: bool = true;
#[cfg(not(debug_assertions))]
const DEV_MODE: bool = false;
const DATABASE_KEY_NAME: &str = "email_address";
const EMAIL_REGEX: &str = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b";
const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
const FEEDBACK_CHAR_LIMIT: RangeInclusive<i32> = 10..=5000;
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
"Feedback failed to submit, see error log for details.";
@ -41,8 +51,9 @@ pub struct FeedbackModal {
system_specs: SystemSpecs,
feedback_editor: View<Editor>,
email_address_editor: View<Editor>,
character_count: usize,
pending_submission: bool,
awaiting_submission: bool,
user_submitted: bool,
character_count: i32,
}
impl FocusableView for FeedbackModal {
@ -52,6 +63,25 @@ impl FocusableView for FeedbackModal {
}
impl EventEmitter<DismissEvent> for FeedbackModal {}
impl ModalView for FeedbackModal {
fn dismiss(&mut self, cx: &mut ViewContext<Self>) -> Task<bool> {
if self.user_submitted {
self.set_user_submitted(false, cx);
return cx.spawn(|_, _| async { true });
}
let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some();
if !has_feedback {
return cx.spawn(|_, _| async { true });
}
let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", &["Yes", "No"]);
cx.spawn(|_, _| async { answer.await.ok() == Some(0) })
}
}
impl FeedbackModal {
pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
let _handle = cx.view().downgrade();
@ -104,6 +134,11 @@ impl FeedbackModal {
let feedback_editor = cx.build_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
editor.set_placeholder_text(
"You can use markdown to organize your feedback wiht add code and links, or organize feedback.",
cx,
);
// editor.set_show_gutter(false, cx);
editor.set_vertical_scroll_margin(5, cx);
editor
});
@ -119,7 +154,7 @@ impl FeedbackModal {
.as_singleton()
.expect("Feedback editor is never a multi-buffer")
.read(cx)
.len();
.len() as i32;
cx.notify();
}
_ => {}
@ -131,7 +166,8 @@ impl FeedbackModal {
system_specs: system_specs.clone(),
feedback_editor,
email_address_editor,
pending_submission: false,
awaiting_submission: false,
user_submitted: false,
character_count: 0,
}
}
@ -163,16 +199,25 @@ impl FeedbackModal {
}
};
this.update(&mut cx, |feedback_editor, cx| {
feedback_editor.set_pending_submission(true, cx);
this.update(&mut cx, |this, cx| {
this.set_awaiting_submission(true, cx);
})
.log_err();
if let Err(error) =
FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await
{
let res =
FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await;
match res {
Ok(_) => {
this.update(&mut cx, |this, cx| {
this.set_user_submitted(true, cx);
cx.emit(DismissEvent)
})
.ok();
}
Err(error) => {
log::error!("{}", error);
this.update(&mut cx, |feedback_editor, cx| {
this.update(&mut cx, |this, cx| {
let prompt = cx.prompt(
PromptLevel::Critical,
FEEDBACK_SUBMISSION_ERROR_TEXT,
@ -182,18 +227,25 @@ impl FeedbackModal {
prompt.await.ok();
})
.detach();
feedback_editor.set_pending_submission(false, cx);
this.set_awaiting_submission(false, cx);
})
.log_err();
}
}
}
})
.detach();
Task::ready(Ok(()))
}
fn set_pending_submission(&mut self, pending_submission: bool, cx: &mut ViewContext<Self>) {
self.pending_submission = pending_submission;
fn set_awaiting_submission(&mut self, awaiting_submission: bool, cx: &mut ViewContext<Self>) {
self.awaiting_submission = awaiting_submission;
cx.notify();
}
fn set_user_submitted(&mut self, user_submitted: bool, cx: &mut ViewContext<Self>) {
self.user_submitted = user_submitted;
cx.notify();
}
@ -203,6 +255,14 @@ impl FeedbackModal {
zed_client: Arc<Client>,
system_specs: SystemSpecs,
) -> anyhow::Result<()> {
if DEV_MODE {
if SEND_SUCCESS_IN_DEV_MODE {
return Ok(());
} else {
return Err(anyhow!("Error submitting feedback"));
}
}
let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
let telemetry = zed_client.telemetry();
let metrics_id = telemetry.metrics_id();
@ -233,11 +293,8 @@ impl FeedbackModal {
}
// TODO: Escape button calls dismiss
// TODO: Should do same as hitting cancel / clicking outside of modal
// Close immediately if no text in field
// Ask to close if text in the field
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
cx.emit(DismissEvent)
}
}
@ -251,132 +308,128 @@ impl Render for FeedbackModal {
};
let valid_character_count = FEEDBACK_CHAR_LIMIT.contains(&self.character_count);
let characters_remaining =
if valid_character_count || self.character_count > *FEEDBACK_CHAR_LIMIT.end() {
*FEEDBACK_CHAR_LIMIT.end() as i32 - self.character_count as i32
} else {
self.character_count as i32 - *FEEDBACK_CHAR_LIMIT.start() as i32
};
let allow_submission =
valid_character_count && valid_email_address && !self.pending_submission;
valid_character_count && valid_email_address && !self.awaiting_submission;
let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some();
let submit_button_text = if self.pending_submission {
"Sending..."
let submit_button_text = if self.awaiting_submission {
"Submitting..."
} else {
"Send Feedback"
"Submit"
};
let dismiss = cx.listener(|_, _, cx| {
cx.emit(DismissEvent);
});
// TODO: get the "are you sure you want to dismiss?" prompt here working
let dismiss_prompt = cx.listener(|_, _, _| {
// let answer = cx.prompt(PromptLevel::Info, "Exit feedback?", &["Yes", "No"]);
// cx.spawn(|_, _| async move {
// let answer = answer.await.ok();
// if answer == Some(0) {
// cx.emit(DismissEvent);
// }
// })
// .detach();
});
let open_community_repo =
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo)));
// TODO: Nate UI pass
// Moved this here because providing it inline breaks rustfmt
let provide_an_email_address =
"Provide an email address if you want us to be able to reply.";
v_stack()
.elevation_3(cx)
.key_context("GiveFeedback")
.on_action(cx.listener(Self::cancel))
.min_w(rems(40.))
.max_w(rems(96.))
.border()
.border_color(red())
.h(rems(40.))
.p_2()
.gap_2()
.h(rems(32.))
.p_4()
.gap_4()
.child(v_stack().child(
// TODO: Add Headline component to `ui2`
div().text_xl().child("Share Feedback"),
))
.child(
v_stack().child(
div()
.size_full()
.child(Label::new("Give Feedback").color(Color::Default))
.child(Label::new("This editor supports markdown").color(Color::Muted)),
),
Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() {
format!(
"Feedback must be at least {} characters.",
FEEDBACK_CHAR_LIMIT.start()
)
} else {
format!(
"Characters: {}",
*FEEDBACK_CHAR_LIMIT.end() - self.character_count
)
})
.color(if valid_character_count {
Color::Success
} else {
Color::Error
}),
)
.child(
div()
.flex_1()
.bg(cx.theme().colors().editor_background)
.p_2()
.border()
.rounded_md()
.border_color(cx.theme().colors().border)
.child(self.feedback_editor.clone()),
)
.child(
div().child(
Label::new(format!(
"Characters: {}",
characters_remaining
))
.when_else(
valid_character_count,
|this| this.color(Color::Success),
|this| this.color(Color::Error)
)
),
)
.child(
div()
.child(
h_stack()
.bg(cx.theme().colors().editor_background)
.p_2()
.border()
.rounded_md()
.border_color(cx.theme().colors().border)
.child(self.email_address_editor.clone())
.child(self.email_address_editor.clone()),
)
.child(
h_stack()
.justify_between()
.gap_1()
.child(Button::new("community_repo", "Community Repo")
.style(ButtonStyle::Filled)
.color(Color::Muted)
.on_click(open_community_repo)
.child(
Button::new("community_repo", "Community Repo")
.style(ButtonStyle::Transparent)
.icon(Icon::ExternalLink)
.icon_position(IconPosition::End)
.icon_size(IconSize::Small)
.on_click(open_community_repo),
)
.child(h_stack().justify_between().gap_1()
.child(
h_stack()
.gap_1()
.child(
Button::new("cancel_feedback", "Cancel")
.style(ButtonStyle::Subtle)
.color(Color::Muted)
// TODO: replicate this logic when clicking outside the modal
// TODO: Will require somehow overriding the modal dismal default behavior
.when_else(
has_feedback,
|this| this.on_click(dismiss_prompt),
|this| this.on_click(dismiss)
)
.on_click(cx.listener(move |_, _, cx| {
cx.spawn(|this, mut cx| async move {
this.update(&mut cx, |_, cx| {
cx.emit(DismissEvent)
})
.ok();
})
.detach();
})),
)
.child(
Button::new("send_feedback", submit_button_text)
.color(Color::Accent)
.style(ButtonStyle::Filled)
// TODO: Ensure that while submitting, "Sending..." is shown and disable the button
// TODO: If submit errors: show popup with error, don't close modal, set text back to "Send Feedback", and re-enable button
// TODO: If submit is successful, close the modal
// TODO: If submit errors: show popup with error, don't close modal, set text back to "Submit", and re-enable button
.on_click(cx.listener(|this, _, cx| {
let _ = this.submit(cx);
this.submit(cx).detach();
}))
.tooltip(|cx| {
.tooltip(move |cx| {
Tooltip::with_meta(
"Submit feedback to the Zed team.",
None,
"Provide an email address if you want us to be able to reply.",
provide_an_email_address,
cx,
)
})
.when(!allow_submission, |this| this.disabled(true))
.when(!allow_submission, |this| this.disabled(true)),
),
),
),
)
}
}
)
}
}
// TODO: Maybe store email address whenever the modal is closed, versus just on submit, so users can remove it if they want without submitting
// TODO: Testing of various button states, dismissal prompts, etc.

View file

@ -17,9 +17,11 @@ use std::{
use text::Point;
use ui::{prelude::*, HighlightedLabel, ListItem};
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
use workspace::Workspace;
use workspace::{ModalView, Workspace};
actions!(Toggle);
actions!(file_finder, [Toggle]);
impl ModalView for FileFinder {}
pub struct FileFinder {
picker: View<Picker<FileFinderDelegate>>,

View file

@ -8,8 +8,9 @@ use text::{Bias, Point};
use theme::ActiveTheme;
use ui::{h_stack, prelude::*, v_stack, Label};
use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::ModalView;
actions!(Toggle);
actions!(go_to_line, [Toggle]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(GoToLine::register).detach();
@ -23,6 +24,8 @@ pub struct GoToLine {
_subscriptions: Vec<Subscription>,
}
impl ModalView for GoToLine {}
impl FocusableView for GoToLine {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.line_editor.focus_handle(cx)

View file

@ -22,7 +22,7 @@ Actions are frequently unit structs, for which we have a macro. The above could
```rust
mod menu {
actions!(MoveUp, MoveDown);
actions!(gpui, [MoveUp, MoveDown]);
}
```

View file

@ -3,34 +3,33 @@ use anyhow::{anyhow, Context, Result};
use collections::HashMap;
pub use no_action::NoAction;
use serde_json::json;
use std::{
any::{Any, TypeId},
ops::Deref,
};
use std::any::{Any, TypeId};
/// Actions are used to implement keyboard-driven UI.
/// When you declare an action, you can bind keys to the action in the keymap and
/// listeners for that action in the element tree.
///
/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit struct
/// action for each listed action name.
/// action for each listed action name in the given namespace.
/// ```rust
/// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline);
/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
/// ```
/// More complex data types can also be actions. If you annotate your type with the action derive macro
/// it will be implemented and registered automatically.
/// More complex data types can also be actions, providing they implement Clone, PartialEq,
/// and serde_derive::Deserialize.
/// Use `impl_actions!` to automatically implement the action in the given namespace.
/// ```
/// #[derive(Clone, PartialEq, serde_derive::Deserialize, Action)]
/// #[derive(Clone, PartialEq, serde_derive::Deserialize)]
/// pub struct SelectNext {
/// pub replace_newest: bool,
/// }
/// impl_actions!(editor, [SelectNext]);
/// ```
///
/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
/// macro, which only generates the code needed to register your action before `main`.
///
/// ```
/// #[gpui::register_action]
/// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::fmt::Debug)]
/// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)]
/// pub struct Paste {
/// pub content: SharedString,
/// }
@ -38,6 +37,7 @@ use std::{
/// impl gpui::Action for Paste {
/// ///...
/// }
/// register_action!(Paste);
/// ```
pub trait Action: 'static {
fn boxed_clone(&self) -> Box<dyn Action>;
@ -56,7 +56,7 @@ pub trait Action: 'static {
impl std::fmt::Debug for dyn Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn Action")
.field("type_name", &self.name())
.field("name", &self.name())
.finish()
}
}
@ -115,7 +115,7 @@ impl ActionRegistry {
for builder in __GPUI_ACTIONS {
let action = builder();
//todo(remove)
let name: SharedString = remove_the_2(action.name).into();
let name: SharedString = action.name.into();
self.builders_by_name.insert(name.clone(), action.build);
self.names_by_type_id.insert(action.type_id, name.clone());
self.all_names.push(name);
@ -139,11 +139,9 @@ impl ActionRegistry {
name: &str,
params: Option<serde_json::Value>,
) -> Result<Box<dyn Action>> {
//todo(remove)
let name = remove_the_2(name);
let build_action = self
.builders_by_name
.get(name.deref())
.get(name)
.ok_or_else(|| anyhow!("no action type registered for {}", name))?;
(build_action)(params.unwrap_or_else(|| json!({})))
.with_context(|| format!("Attempting to build action {}", name))
@ -155,36 +153,88 @@ impl ActionRegistry {
}
/// Defines unit structs that can be used as actions.
/// To use more complex data types as actions, annotate your type with the #[action] macro.
/// To use more complex data types as actions, use `impl_actions!`
#[macro_export]
macro_rules! actions {
() => {};
( $name:ident ) => {
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize)]
#[serde(crate = "gpui::serde")]
pub struct $name;
};
( $name:ident, $($rest:tt)* ) => {
actions!($name);
actions!($($rest)*);
gpui::__impl_action!($namespace, $name,
fn build(_: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
Ok(Box::new(Self))
}
);
gpui::register_action!($name);
)*
};
}
//todo!(remove)
pub fn remove_the_2(action_name: &str) -> String {
let mut separator_matches = action_name.rmatch_indices("::");
separator_matches.next().unwrap();
let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
// todo!() remove the 2 replacement when migration is done
action_name[name_start_ix..]
.replace("2::", "::")
.to_string()
/// Implements the Action trait for any struct that implements Clone, Default, PartialEq, and serde_deserialize::Deserialize
#[macro_export]
macro_rules! impl_actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
gpui::__impl_action!($namespace, $name,
fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
}
);
)*
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __impl_action {
($namespace:path, $name:ident, $build:item) => {
impl gpui::Action for $name {
fn name(&self) -> &'static str
{
concat!(
stringify!($namespace),
"::",
stringify!($name),
)
}
// todo!() why is this needed in addition to name?
fn debug_name() -> &'static str
where
Self: ::std::marker::Sized
{
concat!(
stringify!($namespace),
"::",
stringify!($name),
)
}
$build
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
action
.as_any()
.downcast_ref::<Self>()
.map_or(false, |a| self == a)
}
fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
::std::boxed::Box::new(self.clone())
}
fn as_any(&self) -> &dyn ::std::any::Any {
self
}
}
};
}
mod no_action {
use crate as gpui;
actions!(NoAction);
actions!(zed, [NoAction]);
}

View file

@ -13,6 +13,7 @@ use smallvec::SmallVec;
use smol::future::FutureExt;
#[cfg(any(test, feature = "test-support"))]
pub use test_context::*;
use time::UtcOffset;
use crate::{
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
@ -536,6 +537,10 @@ impl AppContext {
self.platform.restart()
}
pub fn local_timezone(&self) -> UtcOffset {
self.platform.local_timezone()
}
pub(crate) fn push_effect(&mut self, effect: Effect) {
match &effect {
Effect::Notify { emitter } => {
@ -1110,6 +1115,10 @@ impl AppContext {
}
}
}
pub fn has_active_drag(&self) -> bool {
self.active_drag.is_some()
}
}
impl Context for AppContext {

View file

@ -69,24 +69,6 @@ pub trait IntoElement: Sized {
self.map(|this| if condition { then(this) } else { this })
}
fn when_else(
self,
condition: bool,
then: impl FnOnce(Self) -> Self,
otherwise: impl FnOnce(Self) -> Self,
) -> Self
where
Self: Sized,
{
self.map(|this| {
if condition {
then(this)
} else {
otherwise(this)
}
})
}
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
where
Self: Sized,

View file

@ -0,0 +1,496 @@
use crate::{
px, AnyElement, AvailableSpace, BorrowAppContext, DispatchPhase, Element, IntoElement, Pixels,
Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, WindowContext,
};
use collections::VecDeque;
use refineable::Refineable as _;
use std::{cell::RefCell, ops::Range, rc::Rc};
use sum_tree::{Bias, SumTree};
pub fn list(state: ListState) -> List {
List {
state,
style: StyleRefinement::default(),
}
}
pub struct List {
state: ListState,
style: StyleRefinement,
}
#[derive(Clone)]
pub struct ListState(Rc<RefCell<StateInner>>);
struct StateInner {
last_layout_width: Option<Pixels>,
render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
items: SumTree<ListItem>,
logical_scroll_top: Option<ListOffset>,
alignment: ListAlignment,
overdraw: Pixels,
#[allow(clippy::type_complexity)]
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ListAlignment {
Top,
Bottom,
}
pub struct ListScrollEvent {
pub visible_range: Range<usize>,
pub count: usize,
}
#[derive(Clone)]
enum ListItem {
Unrendered,
Rendered { height: Pixels },
}
#[derive(Clone, Debug, Default, PartialEq)]
struct ListItemSummary {
count: usize,
rendered_count: usize,
unrendered_count: usize,
height: Pixels,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct Count(usize);
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct RenderedCount(usize);
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct UnrenderedCount(usize);
#[derive(Clone, Debug, Default)]
struct Height(Pixels);
impl ListState {
pub fn new<F>(
element_count: usize,
orientation: ListAlignment,
overdraw: Pixels,
render_item: F,
) -> Self
where
F: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
{
let mut items = SumTree::new();
items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
Self(Rc::new(RefCell::new(StateInner {
last_layout_width: None,
render_item: Box::new(render_item),
items,
logical_scroll_top: None,
alignment: orientation,
overdraw,
scroll_handler: None,
})))
}
pub fn reset(&self, element_count: usize) {
let state = &mut *self.0.borrow_mut();
state.logical_scroll_top = None;
state.items = SumTree::new();
state
.items
.extend((0..element_count).map(|_| ListItem::Unrendered), &());
}
pub fn item_count(&self) -> usize {
self.0.borrow().items.summary().count
}
pub fn splice(&self, old_range: Range<usize>, count: usize) {
let state = &mut *self.0.borrow_mut();
if let Some(ListOffset {
item_ix,
offset_in_item,
}) = state.logical_scroll_top.as_mut()
{
if old_range.contains(item_ix) {
*item_ix = old_range.start;
*offset_in_item = px(0.);
} else if old_range.end <= *item_ix {
*item_ix = *item_ix - (old_range.end - old_range.start) + count;
}
}
let mut old_heights = state.items.cursor::<Count>();
let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
new_heights.append(old_heights.suffix(&()), &());
drop(old_heights);
state.items = new_heights;
}
pub fn set_scroll_handler(
&self,
handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
) {
self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
}
pub fn logical_scroll_top(&self) -> ListOffset {
self.0.borrow().logical_scroll_top()
}
pub fn scroll_to(&self, mut scroll_top: ListOffset) {
let state = &mut *self.0.borrow_mut();
let item_count = state.items.summary().count;
if scroll_top.item_ix >= item_count {
scroll_top.item_ix = item_count;
scroll_top.offset_in_item = px(0.);
}
state.logical_scroll_top = Some(scroll_top);
}
}
impl StateInner {
fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
let start_y = cursor.start().height + scroll_top.offset_in_item;
cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
scroll_top.item_ix..cursor.start().count + 1
}
fn scroll(
&mut self,
scroll_top: &ListOffset,
height: Pixels,
delta: Point<Pixels>,
cx: &mut WindowContext,
) {
let scroll_max = (self.items.summary().height - height).max(px(0.));
let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
.max(px(0.))
.min(scroll_max);
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
self.logical_scroll_top = None;
} else {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Height(new_scroll_top), Bias::Right, &());
let item_ix = cursor.start().count;
let offset_in_item = new_scroll_top - cursor.start().height;
self.logical_scroll_top = Some(ListOffset {
item_ix,
offset_in_item,
});
}
if self.scroll_handler.is_some() {
let visible_range = self.visible_range(height, scroll_top);
self.scroll_handler.as_mut().unwrap()(
&ListScrollEvent {
visible_range,
count: self.items.summary().count,
},
cx,
);
}
cx.notify();
}
fn logical_scroll_top(&self) -> ListOffset {
self.logical_scroll_top
.unwrap_or_else(|| match self.alignment {
ListAlignment::Top => ListOffset {
item_ix: 0,
offset_in_item: px(0.),
},
ListAlignment::Bottom => ListOffset {
item_ix: self.items.summary().count,
offset_in_item: px(0.),
},
})
}
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
cursor.start().height + logical_scroll_top.offset_in_item
}
}
impl std::fmt::Debug for ListItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unrendered => write!(f, "Unrendered"),
Self::Rendered { height, .. } => {
f.debug_struct("Rendered").field("height", height).finish()
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ListOffset {
pub item_ix: usize,
pub offset_in_item: Pixels,
}
impl Element for List {
type State = ();
fn layout(
&mut self,
_state: Option<Self::State>,
cx: &mut crate::WindowContext,
) -> (crate::LayoutId, Self::State) {
let mut style = Style::default();
style.refine(&self.style);
let layout_id = cx.with_text_style(style.text_style().cloned(), |cx| {
cx.request_layout(&style, None)
});
(layout_id, ())
}
fn paint(
self,
bounds: crate::Bounds<crate::Pixels>,
_state: &mut Self::State,
cx: &mut crate::WindowContext,
) {
let state = &mut *self.state.0.borrow_mut();
// If the width of the list has changed, invalidate all cached item heights
if state.last_layout_width != Some(bounds.size.width) {
state.items = SumTree::from_iter(
(0..state.items.summary().count).map(|_| ListItem::Unrendered),
&(),
)
}
let old_items = state.items.clone();
let mut measured_items = VecDeque::new();
let mut item_elements = VecDeque::new();
let mut rendered_height = px(0.);
let mut scroll_top = state.logical_scroll_top();
let available_item_space = Size {
width: AvailableSpace::Definite(bounds.size.width),
height: AvailableSpace::MinContent,
};
// Render items after the scroll top, including those in the trailing overdraw
let mut cursor = old_items.cursor::<Count>();
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
for (ix, item) in cursor.by_ref().enumerate() {
let visible_height = rendered_height - scroll_top.offset_in_item;
if visible_height >= bounds.size.height + state.overdraw {
break;
}
// Use the previously cached height if available
let mut height = if let ListItem::Rendered { height } = item {
Some(*height)
} else {
None
};
// If we're within the visible area or the height wasn't cached, render and measure the item's element
if visible_height < bounds.size.height || height.is_none() {
let mut element = (state.render_item)(scroll_top.item_ix + ix, cx);
let element_size = element.measure(available_item_space, cx);
height = Some(element_size.height);
if visible_height < bounds.size.height {
item_elements.push_back(element);
}
}
let height = height.unwrap();
rendered_height += height;
measured_items.push_back(ListItem::Rendered { height });
}
// Prepare to start walking upward from the item at the scroll top.
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
// If the rendered items do not fill the visible region, then adjust
// the scroll top upward.
if rendered_height - scroll_top.offset_in_item < bounds.size.height {
while rendered_height < bounds.size.height {
cursor.prev(&());
if cursor.item().is_some() {
let mut element = (state.render_item)(cursor.start().0, cx);
let element_size = element.measure(available_item_space, cx);
rendered_height += element_size.height;
measured_items.push_front(ListItem::Rendered {
height: element_size.height,
});
item_elements.push_front(element)
} else {
break;
}
}
scroll_top = ListOffset {
item_ix: cursor.start().0,
offset_in_item: rendered_height - bounds.size.height,
};
match state.alignment {
ListAlignment::Top => {
scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
state.logical_scroll_top = Some(scroll_top);
}
ListAlignment::Bottom => {
scroll_top = ListOffset {
item_ix: cursor.start().0,
offset_in_item: rendered_height - bounds.size.height,
};
state.logical_scroll_top = None;
}
};
}
// Measure items in the leading overdraw
let mut leading_overdraw = scroll_top.offset_in_item;
while leading_overdraw < state.overdraw {
cursor.prev(&());
if let Some(item) = cursor.item() {
let height = if let ListItem::Rendered { height } = item {
*height
} else {
let mut element = (state.render_item)(cursor.start().0, cx);
element.measure(available_item_space, cx).height
};
leading_overdraw += height;
measured_items.push_front(ListItem::Rendered { height });
} else {
break;
}
}
let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
let mut cursor = old_items.cursor::<Count>();
let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
new_items.extend(measured_items, &());
cursor.seek(&Count(measured_range.end), Bias::Right, &());
new_items.append(cursor.suffix(&()), &());
// Paint the visible items
let mut item_origin = bounds.origin;
item_origin.y -= scroll_top.offset_in_item;
for mut item_element in item_elements {
let item_height = item_element.measure(available_item_space, cx).height;
item_element.draw(item_origin, available_item_space, cx);
item_origin.y += item_height;
}
state.items = new_items;
state.last_layout_width = Some(bounds.size.width);
let list_state = self.state.clone();
let height = bounds.size.height;
cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
list_state.0.borrow_mut().scroll(
&scroll_top,
height,
event.delta.pixel_delta(px(20.)),
cx,
)
}
});
}
}
impl IntoElement for List {
type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> {
None
}
fn into_element(self) -> Self::Element {
self
}
}
impl Styled for List {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl sum_tree::Item for ListItem {
type Summary = ListItemSummary;
fn summary(&self) -> Self::Summary {
match self {
ListItem::Unrendered => ListItemSummary {
count: 1,
rendered_count: 0,
unrendered_count: 1,
height: px(0.),
},
ListItem::Rendered { height } => ListItemSummary {
count: 1,
rendered_count: 1,
unrendered_count: 0,
height: *height,
},
}
}
}
impl sum_tree::Summary for ListItemSummary {
type Context = ();
fn add_summary(&mut self, summary: &Self, _: &()) {
self.count += summary.count;
self.rendered_count += summary.rendered_count;
self.unrendered_count += summary.unrendered_count;
self.height += summary.height;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.rendered_count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.unrendered_count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.height;
}
}
impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
self.0.partial_cmp(&other.count).unwrap()
}
}
impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
self.0.partial_cmp(&other.height).unwrap()
}
}

View file

@ -1,6 +1,7 @@
mod canvas;
mod div;
mod img;
mod list;
mod overlay;
mod svg;
mod text;
@ -9,6 +10,7 @@ mod uniform_list;
pub use canvas::*;
pub use div::*;
pub use img::*;
pub use list::*;
pub use overlay::*;
pub use svg::*;
pub use text::*;

View file

@ -131,7 +131,7 @@ impl Element for UniformList {
}
});
let height = match available_space.height {
AvailableSpace::Definite(x) => desired_height.min(x),
AvailableSpace::Definite(height) => desired_height.min(height),
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
desired_height
}

View file

@ -1,6 +1,7 @@
#[macro_use]
mod action;
mod app;
mod assets;
mod color;
mod element;
@ -15,6 +16,7 @@ mod keymap;
mod platform;
pub mod prelude;
mod scene;
mod shared_string;
mod style;
mod styled;
mod subscription;
@ -57,6 +59,7 @@ pub use scene::*;
pub use serde;
pub use serde_derive;
pub use serde_json;
pub use shared_string::*;
pub use smallvec;
pub use smol::Timer;
pub use style::*;
@ -71,10 +74,9 @@ pub use util::arc_cow::ArcCow;
pub use view::*;
pub use window::*;
use derive_more::{Deref, DerefMut};
use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut},
borrow::BorrowMut,
};
use taffy::TaffyLayoutEngine;
@ -209,42 +211,3 @@ impl<T> Flatten<T> for Result<T> {
self
}
}
#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
pub struct SharedString(ArcCow<'static, str>);
impl Default for SharedString {
fn default() -> Self {
Self(ArcCow::Owned("".into()))
}
}
impl AsRef<str> for SharedString {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for SharedString {
fn borrow(&self) -> &str {
self.as_ref()
}
}
impl std::fmt::Debug for SharedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::fmt::Display for SharedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.as_ref())
}
}
impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
fn from(value: T) -> Self {
Self(value.into())
}
}

View file

@ -193,6 +193,12 @@ impl Deref for MouseExitEvent {
#[derive(Debug, Clone, Default)]
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
impl ExternalPaths {
pub fn paths(&self) -> &[PathBuf] {
&self.0
}
}
impl Render for ExternalPaths {
type Element = Div;
@ -296,7 +302,7 @@ mod test {
focus_handle: FocusHandle,
}
actions!(TestAction);
actions!(test, [TestAction]);
impl Render for TestView {
type Element = Stateful<Div>;

View file

@ -149,13 +149,19 @@ impl DispatchTree {
}
pub fn available_actions(&self, target: DispatchNodeId) -> Vec<Box<dyn Action>> {
let mut actions = Vec::new();
let mut actions = Vec::<Box<dyn Action>>::new();
for node_id in self.dispatch_path(target) {
let node = &self.nodes[node_id.0];
for DispatchActionListener { action_type, .. } in &node.action_listeners {
if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id())
{
// Intentionally silence these errors without logging.
// If an action cannot be built by default, it's not available.
actions.extend(self.action_registry.build_action_type(action_type).ok());
let action = self.action_registry.build_action_type(action_type).ok();
if let Some(action) = action {
actions.insert(ix, action);
}
}
}
}
actions

View file

@ -293,11 +293,13 @@ mod tests {
#[test]
fn test_actions_definition() {
{
actions!(A, B, C, D, E, F, G);
actions!(test, [A, B, C, D, E, F, G]);
}
{
actions!(
test,
[
A,
B,
C,
@ -305,6 +307,7 @@ mod tests {
E,
F,
G, // Don't wrap, test the trailing comma
]
);
}
}

View file

@ -325,7 +325,7 @@ impl MetalRenderer {
.entry(tile.texture_id)
.or_insert(Vec::new())
.extend(path.vertices.iter().map(|vertex| PathVertex {
xy_position: vertex.xy_position - path.bounds.origin
xy_position: vertex.xy_position - clipped_bounds.origin
+ tile.bounds.origin.map(Into::into),
st_position: vertex.st_position,
content_mask: ContentMask {
@ -544,9 +544,10 @@ impl MetalRenderer {
if let Some((path, tile)) = paths_and_tiles.peek() {
if prev_texture_id.map_or(true, |texture_id| texture_id == tile.texture_id) {
prev_texture_id = Some(tile.texture_id);
let origin = path.bounds.intersect(&path.content_mask.bounds).origin;
sprites.push(PathSprite {
bounds: Bounds {
origin: path.bounds.origin.map(|p| p.floor()),
origin: origin.map(|p| p.floor()),
size: tile.bounds.size.map(Into::into),
},
color: path.color,

View file

@ -0,0 +1,101 @@
use derive_more::{Deref, DerefMut};
use serde::{Deserialize, Serialize};
use std::{borrow::Borrow, sync::Arc};
use util::arc_cow::ArcCow;
#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
pub struct SharedString(ArcCow<'static, str>);
impl Default for SharedString {
fn default() -> Self {
Self(ArcCow::Owned("".into()))
}
}
impl AsRef<str> for SharedString {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for SharedString {
fn borrow(&self) -> &str {
self.as_ref()
}
}
impl std::fmt::Debug for SharedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::fmt::Display for SharedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.as_ref())
}
}
impl PartialEq<String> for SharedString {
fn eq(&self, other: &String) -> bool {
self.as_ref() == other
}
}
impl PartialEq<SharedString> for String {
fn eq(&self, other: &SharedString) -> bool {
self == other.as_ref()
}
}
impl PartialEq<str> for SharedString {
fn eq(&self, other: &str) -> bool {
self.as_ref() == other
}
}
impl<'a> PartialEq<&'a str> for SharedString {
fn eq(&self, other: &&'a str) -> bool {
self.as_ref() == *other
}
}
impl Into<Arc<str>> for SharedString {
fn into(self) -> Arc<str> {
match self.0 {
ArcCow::Borrowed(borrowed) => Arc::from(borrowed),
ArcCow::Owned(owned) => owned.clone(),
}
}
}
impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
fn from(value: T) -> Self {
Self(value.into())
}
}
impl Into<String> for SharedString {
fn into(self) -> String {
self.0.to_string()
}
}
impl Serialize for SharedString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl<'de> Deserialize<'de> for SharedString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(SharedString::from(s))
}
}

View file

@ -245,6 +245,13 @@ pub trait Styled: Sized {
self
}
/// Sets the flex direction of the element to `column-reverse`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#column-reverse)
fn flex_col_reverse(mut self) -> Self {
self.style().flex_direction = Some(FlexDirection::ColumnReverse);
self
}
/// Sets the flex direction of the element to `row`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#row)
fn flex_row(mut self) -> Self {
@ -252,6 +259,13 @@ pub trait Styled: Sized {
self
}
/// Sets the flex direction of the element to `row-reverse`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#row-reverse)
fn flex_row_reverse(mut self) -> Self {
self.style().flex_direction = Some(FlexDirection::RowReverse);
self
}
/// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size.
/// [Docs](https://tailwindcss.com/docs/flex#flex-1)
fn flex_1(mut self) -> Self {

View file

@ -4,12 +4,12 @@ use crate::{
DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model,
ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler,
PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline,
UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent, Path,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet,
Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Context as _, Result};
use collections::HashMap;
@ -1269,10 +1269,9 @@ impl<'a> WindowContext<'a> {
cursor_offset: position,
});
}
InputEvent::MouseDown(MouseDownEvent {
InputEvent::MouseMove(MouseMoveEvent {
position,
button: MouseButton::Left,
click_count: 1,
pressed_button: Some(MouseButton::Left),
modifiers: Modifiers::default(),
})
}
@ -1285,6 +1284,7 @@ impl<'a> WindowContext<'a> {
})
}
FileDropEvent::Submit { position } => {
self.activate(true);
self.window.mouse_position = position;
InputEvent::MouseUp(MouseUpEvent {
button: MouseButton::Left,
@ -2839,3 +2839,9 @@ impl From<(&'static str, usize)> for ElementId {
ElementId::NamedInteger(name.into(), id)
}
}
impl From<(&'static str, u64)> for ElementId {
fn from((name, id): (&'static str, u64)) -> Self {
ElementId::NamedInteger(name.into(), id as usize)
}
}

View file

@ -1,16 +1,23 @@
use gpui2::{actions, impl_actions};
use gpui2_macros::register_action;
use serde_derive::Deserialize;
#[test]
fn test_derive() {
fn test_action_macros() {
use gpui2 as gpui;
#[derive(PartialEq, Clone, Deserialize, gpui2_macros::Action)]
actions!(test, [TestAction]);
#[derive(PartialEq, Clone, Deserialize)]
struct AnotherTestAction;
#[gpui2_macros::register_action]
impl_actions!(test, [AnotherTestAction]);
#[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)]
struct RegisterableAction {}
register_action!(RegisterableAction);
impl gpui::Action for RegisterableAction {
fn boxed_clone(&self) -> Box<dyn gpui::Action> {
todo!()

View file

@ -1,96 +0,0 @@
// Input:
//
// #[action]
// struct Foo {
// bar: String,
// }
// Output:
//
// #[gpui::register_action]
// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)]
// struct Foo {
// bar: String,
// }
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Error};
use crate::register_action::register_action;
pub fn action(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
if input.generics.lt_token.is_some() {
return Error::new(name.span(), "Actions must be a concrete type")
.into_compile_error()
.into();
}
let is_unit_struct = match input.data {
syn::Data::Struct(struct_data) => struct_data.fields.is_empty(),
syn::Data::Enum(_) => false,
syn::Data::Union(_) => false,
};
let build_impl = if is_unit_struct {
quote! {
Ok(std::boxed::Box::new(Self {}))
}
} else {
quote! {
Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
}
};
let register_action = register_action(&name);
let output = quote! {
const _: fn() = || {
fn assert_impl<T: ?Sized + for<'a> gpui::serde::Deserialize<'a> + ::std::cmp::PartialEq + ::std::clone::Clone>() {}
assert_impl::<#name>();
};
impl gpui::Action for #name {
fn name(&self) -> &'static str
{
::std::any::type_name::<#name>()
}
fn debug_name() -> &'static str
where
Self: ::std::marker::Sized
{
::std::any::type_name::<#name>()
}
fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>>
where
Self: ::std::marker::Sized {
#build_impl
}
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
action
.as_any()
.downcast_ref::<Self>()
.map_or(false, |a| self == a)
}
fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
::std::boxed::Box::new(self.clone())
}
fn as_any(&self) -> &dyn ::std::any::Any {
self
}
}
#register_action
};
TokenStream::from(output)
}

View file

@ -1,4 +1,3 @@
mod action;
mod derive_into_element;
mod register_action;
mod style_helpers;
@ -6,14 +5,9 @@ mod test;
use proc_macro::TokenStream;
#[proc_macro_derive(Action)]
pub fn action(input: TokenStream) -> TokenStream {
action::action(input)
}
#[proc_macro_attribute]
pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
register_action::register_action_macro(attr, item)
#[proc_macro]
pub fn register_action(ident: TokenStream) -> TokenStream {
register_action::register_action_macro(ident)
}
#[proc_macro_derive(IntoElement)]

View file

@ -14,47 +14,13 @@
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::{format_ident, quote};
use syn::{parse_macro_input, DeriveInput, Error};
use syn::parse_macro_input;
pub fn register_action_macro(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let registration = register_action(&input.ident);
let has_action_derive = input
.attrs
.iter()
.find(|attr| {
(|| {
let meta = attr.parse_meta().ok()?;
meta.path().is_ident("derive").then(|| match meta {
syn::Meta::Path(_) => None,
syn::Meta::NameValue(_) => None,
syn::Meta::List(list) => list
.nested
.iter()
.find(|list| match list {
syn::NestedMeta::Meta(meta) => meta.path().is_ident("Action"),
syn::NestedMeta::Lit(_) => false,
})
.map(|_| true),
})?
})()
.unwrap_or(false)
})
.is_some();
if has_action_derive {
return Error::new(
input.ident.span(),
"The Action derive macro has already registered this action",
)
.into_compile_error()
.into();
}
pub fn register_action_macro(ident: TokenStream) -> TokenStream {
let name = parse_macro_input!(ident as Ident);
let registration = register_action(&name);
TokenStream::from(quote! {
#input
#registration
})
}
@ -78,7 +44,7 @@ pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
#[doc(hidden)]
fn #action_builder_fn_name() -> gpui::ActionData {
gpui::ActionData {
name: ::std::any::type_name::<#type_name>(),
name: <#type_name as gpui::Action>::debug_name(),
type_id: ::std::any::TypeId::of::<#type_name>(),
build: <#type_name as gpui::Action>::build,
}

View file

@ -3,7 +3,7 @@ use gpui::{actions, AsyncAppContext};
use std::path::Path;
use util::ResultExt;
actions!(Install);
actions!(cli, [Install]);
pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??;

View file

@ -14,9 +14,9 @@ use project::Project;
use std::sync::Arc;
use ui::{prelude::*, HighlightedLabel, ListItem};
use util::ResultExt;
use workspace::Workspace;
use workspace::{ModalView, Workspace};
actions!(Toggle);
actions!(language_selector, [Toggle]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(LanguageSelector::register).detach();
@ -81,6 +81,7 @@ impl FocusableView for LanguageSelector {
}
impl EventEmitter<DismissEvent> for LanguageSelector {}
impl ModalView for LanguageSelector {}
pub struct LanguageSelectorDelegate {
language_selector: WeakView<LanguageSelector>,

View file

@ -1,17 +1,15 @@
use std::{sync::Arc, time::Duration};
use futures::StreamExt;
use gpui::{Action, KeyBinding};
use gpui::{actions, KeyBinding};
use live_kit_client2::{
LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
};
use live_kit_server::token::{self, VideoGrant};
use log::LevelFilter;
use serde_derive::Deserialize;
use simplelog::SimpleLogger;
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Action)]
struct Quit;
actions!(live_kit_client, [Quit]);
fn main() {
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");

View file

@ -10,6 +10,8 @@ use gpui::actions;
pub fn init() {}
actions!(
menu,
[
Cancel,
Confirm,
SecondaryConfirm,
@ -18,4 +20,5 @@ actions!(
SelectFirst,
SelectLast,
ShowContextMenu
]
);

View file

@ -1,6 +1,6 @@
use editor::{
display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt,
DisplayPoint, Editor, ToPoint,
DisplayPoint, Editor, EditorMode, ToPoint,
};
use fuzzy::StringMatch;
use gpui::{
@ -20,19 +20,15 @@ use std::{
use theme::{color_alpha, ActiveTheme, ThemeSettings};
use ui::{prelude::*, ListItem};
use util::ResultExt;
use workspace::Workspace;
use workspace::ModalView;
actions!(Toggle);
actions!(outline, [Toggle]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(OutlineView::register).detach();
}
pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
if let Some(editor) = workspace
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
{
pub fn toggle(editor: View<Editor>, _: &Toggle, cx: &mut WindowContext) {
let outline = editor
.read(cx)
.buffer()
@ -40,9 +36,10 @@ pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Worksp
.snapshot(cx)
.outline(Some(&cx.theme().syntax()));
if let Some(outline) = outline {
if let Some((workspace, outline)) = editor.read(cx).workspace().zip(outline) {
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |cx| OutlineView::new(outline, editor, cx));
}
})
}
}
@ -57,6 +54,7 @@ impl FocusableView for OutlineView {
}
impl EventEmitter<DismissEvent> for OutlineView {}
impl ModalView for OutlineView {}
impl Render for OutlineView {
type Element = Div;
@ -67,8 +65,15 @@ impl Render for OutlineView {
}
impl OutlineView {
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(toggle);
fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
if editor.mode() == EditorMode::Full {
let handle = cx.view().downgrade();
editor.register_action(move |action, cx| {
if let Some(editor) = handle.upgrade() {
toggle(editor, action, cx);
}
});
}
}
fn new(
@ -238,6 +243,7 @@ impl PickerDelegate for OutlineViewDelegate {
s.select_ranges([position..position])
});
active_editor.highlight_rows(None);
active_editor.focus(cx);
}
});

View file

@ -33,7 +33,7 @@ pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
}
#[async_trait(?Send)]
pub(crate) trait LspCommand: 'static + Sized {
pub trait LspCommand: 'static + Sized {
type Response: 'static + Default + Send;
type LspRequest: 'static + Send + lsp::request::Request;
type ProtoRequest: 'static + Send + proto::RequestMessage;

View file

@ -0,0 +1,137 @@
use std::{path::Path, sync::Arc};
use anyhow::Context;
use async_trait::async_trait;
use gpui::{AppContext, AsyncAppContext, ModelHandle};
use language::{point_to_lsp, proto::deserialize_anchor, Buffer};
use lsp::{LanguageServer, LanguageServerId};
use rpc::proto::{self, PeerId};
use serde::{Deserialize, Serialize};
use text::{PointUtf16, ToPointUtf16};
use crate::{lsp_command::LspCommand, Project};
pub enum LspExpandMacro {}
impl lsp::request::Request for LspExpandMacro {
type Params = ExpandMacroParams;
type Result = Option<ExpandedMacro>;
const METHOD: &'static str = "rust-analyzer/expandMacro";
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ExpandMacroParams {
pub text_document: lsp::TextDocumentIdentifier,
pub position: lsp::Position,
}
#[derive(Default, Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ExpandedMacro {
pub name: String,
pub expansion: String,
}
impl ExpandedMacro {
pub fn is_empty(&self) -> bool {
self.name.is_empty() && self.expansion.is_empty()
}
}
pub struct ExpandMacro {
pub position: PointUtf16,
}
#[async_trait(?Send)]
impl LspCommand for ExpandMacro {
type Response = ExpandedMacro;
type LspRequest = LspExpandMacro;
type ProtoRequest = proto::LspExtExpandMacro;
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_: &AppContext,
) -> ExpandMacroParams {
ExpandMacroParams {
text_document: lsp::TextDocumentIdentifier {
uri: lsp::Url::from_file_path(path).unwrap(),
},
position: point_to_lsp(self.position),
}
}
async fn response_from_lsp(
self,
message: Option<ExpandedMacro>,
_: ModelHandle<Project>,
_: ModelHandle<Buffer>,
_: LanguageServerId,
_: AsyncAppContext,
) -> anyhow::Result<ExpandedMacro> {
Ok(message
.map(|message| ExpandedMacro {
name: message.name,
expansion: message.expansion,
})
.unwrap_or_default())
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtExpandMacro {
proto::LspExtExpandMacro {
project_id,
buffer_id: buffer.remote_id(),
position: Some(language::proto::serialize_anchor(
&buffer.anchor_before(self.position),
)),
}
}
async fn from_proto(
message: Self::ProtoRequest,
_: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
mut cx: AsyncAppContext,
) -> anyhow::Result<Self> {
let position = message
.position
.and_then(deserialize_anchor)
.context("invalid position")?;
Ok(Self {
position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer)),
})
}
fn response_to_proto(
response: ExpandedMacro,
_: &mut Project,
_: PeerId,
_: &clock::Global,
_: &mut AppContext,
) -> proto::LspExtExpandMacroResponse {
proto::LspExtExpandMacroResponse {
name: response.name,
expansion: response.expansion,
}
}
async fn response_from_proto(
self,
message: proto::LspExtExpandMacroResponse,
_: ModelHandle<Project>,
_: ModelHandle<Buffer>,
_: AsyncAppContext,
) -> anyhow::Result<ExpandedMacro> {
Ok(ExpandedMacro {
name: message.name,
expansion: message.expansion,
})
}
fn buffer_id_from_proto(message: &proto::LspExtExpandMacro) -> u64 {
message.buffer_id
}
}

View file

@ -1,5 +1,6 @@
mod ignore;
mod lsp_command;
pub mod lsp_command;
pub mod lsp_ext_command;
mod prettier_support;
pub mod project_settings;
pub mod search;
@ -174,7 +175,7 @@ struct DelayedDebounced {
cancel_channel: Option<oneshot::Sender<()>>,
}
enum LanguageServerToQuery {
pub enum LanguageServerToQuery {
Primary,
Other(LanguageServerId),
}
@ -626,6 +627,7 @@ impl Project {
client.add_model_request_handler(Self::handle_open_buffer_by_path);
client.add_model_request_handler(Self::handle_save_buffer);
client.add_model_message_handler(Self::handle_update_diff_base);
client.add_model_request_handler(Self::handle_lsp_command::<lsp_ext_command::ExpandMacro>);
}
pub fn local(
@ -5863,7 +5865,7 @@ impl Project {
.await;
}
fn request_lsp<R: LspCommand>(
pub fn request_lsp<R: LspCommand>(
&self,
buffer_handle: ModelHandle<Buffer>,
server: LanguageServerToQuery,

View file

@ -33,7 +33,7 @@ pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
}
#[async_trait(?Send)]
pub(crate) trait LspCommand: 'static + Sized + Send {
pub trait LspCommand: 'static + Sized + Send {
type Response: 'static + Default + Send;
type LspRequest: 'static + Send + lsp::request::Request;
type ProtoRequest: 'static + Send + proto::RequestMessage;

View file

@ -0,0 +1,137 @@
use std::{path::Path, sync::Arc};
use anyhow::Context;
use async_trait::async_trait;
use gpui::{AppContext, AsyncAppContext, Model};
use language::{point_to_lsp, proto::deserialize_anchor, Buffer};
use lsp::{LanguageServer, LanguageServerId};
use rpc::proto::{self, PeerId};
use serde::{Deserialize, Serialize};
use text::{PointUtf16, ToPointUtf16};
use crate::{lsp_command::LspCommand, Project};
pub enum LspExpandMacro {}
impl lsp::request::Request for LspExpandMacro {
type Params = ExpandMacroParams;
type Result = Option<ExpandedMacro>;
const METHOD: &'static str = "rust-analyzer/expandMacro";
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ExpandMacroParams {
pub text_document: lsp::TextDocumentIdentifier,
pub position: lsp::Position,
}
#[derive(Default, Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ExpandedMacro {
pub name: String,
pub expansion: String,
}
impl ExpandedMacro {
pub fn is_empty(&self) -> bool {
self.name.is_empty() && self.expansion.is_empty()
}
}
pub struct ExpandMacro {
pub position: PointUtf16,
}
#[async_trait(?Send)]
impl LspCommand for ExpandMacro {
type Response = ExpandedMacro;
type LspRequest = LspExpandMacro;
type ProtoRequest = proto::LspExtExpandMacro;
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_: &AppContext,
) -> ExpandMacroParams {
ExpandMacroParams {
text_document: lsp::TextDocumentIdentifier {
uri: lsp::Url::from_file_path(path).unwrap(),
},
position: point_to_lsp(self.position),
}
}
async fn response_from_lsp(
self,
message: Option<ExpandedMacro>,
_: Model<Project>,
_: Model<Buffer>,
_: LanguageServerId,
_: AsyncAppContext,
) -> anyhow::Result<ExpandedMacro> {
Ok(message
.map(|message| ExpandedMacro {
name: message.name,
expansion: message.expansion,
})
.unwrap_or_default())
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtExpandMacro {
proto::LspExtExpandMacro {
project_id,
buffer_id: buffer.remote_id(),
position: Some(language::proto::serialize_anchor(
&buffer.anchor_before(self.position),
)),
}
}
async fn from_proto(
message: Self::ProtoRequest,
_: Model<Project>,
buffer: Model<Buffer>,
mut cx: AsyncAppContext,
) -> anyhow::Result<Self> {
let position = message
.position
.and_then(deserialize_anchor)
.context("invalid position")?;
Ok(Self {
position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
})
}
fn response_to_proto(
response: ExpandedMacro,
_: &mut Project,
_: PeerId,
_: &clock::Global,
_: &mut AppContext,
) -> proto::LspExtExpandMacroResponse {
proto::LspExtExpandMacroResponse {
name: response.name,
expansion: response.expansion,
}
}
async fn response_from_proto(
self,
message: proto::LspExtExpandMacroResponse,
_: Model<Project>,
_: Model<Buffer>,
_: AsyncAppContext,
) -> anyhow::Result<ExpandedMacro> {
Ok(ExpandedMacro {
name: message.name,
expansion: message.expansion,
})
}
fn buffer_id_from_proto(message: &proto::LspExtExpandMacro) -> u64 {
message.buffer_id
}
}

View file

@ -1,5 +1,6 @@
mod ignore;
mod lsp_command;
pub mod lsp_command;
pub mod lsp_ext_command;
mod prettier_support;
pub mod project_settings;
pub mod search;
@ -172,7 +173,7 @@ struct DelayedDebounced {
cancel_channel: Option<oneshot::Sender<()>>,
}
enum LanguageServerToQuery {
pub enum LanguageServerToQuery {
Primary,
Other(LanguageServerId),
}
@ -623,6 +624,7 @@ impl Project {
client.add_model_request_handler(Self::handle_open_buffer_by_path);
client.add_model_request_handler(Self::handle_save_buffer);
client.add_model_message_handler(Self::handle_update_diff_base);
client.add_model_request_handler(Self::handle_lsp_command::<lsp_ext_command::ExpandMacro>);
}
pub fn local(
@ -5933,7 +5935,7 @@ impl Project {
.await;
}
fn request_lsp<R: LspCommand>(
pub fn request_lsp<R: LspCommand>(
&self,
buffer_handle: Model<Buffer>,
server: LanguageServerToQuery,

View file

@ -103,6 +103,8 @@ pub struct EntryDetails {
}
actions!(
project_panel,
[
ExpandSelectedEntry,
CollapseSelectedEntry,
CollapseAllEntries,
@ -120,6 +122,7 @@ actions!(
Open,
ToggleFocus,
NewSearchInDirectory,
]
);
pub fn init_settings(cx: &mut AppContext) {

View file

@ -0,0 +1 @@
gpui::actions!(projects, [OpenRecent]);

View file

@ -1,9 +1,10 @@
mod highlighted_workspace_location;
mod projects;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Task,
View, ViewContext, WeakView,
AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Task, View,
ViewContext, WeakView,
};
use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat;
@ -12,11 +13,11 @@ use std::sync::Arc;
use ui::{prelude::*, ListItem};
use util::paths::PathExt;
use workspace::{
notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
WORKSPACE_DB,
notifications::simple_message_notification::MessageNotification, ModalView, Workspace,
WorkspaceLocation, WORKSPACE_DB,
};
actions!(OpenRecent);
pub use projects::OpenRecent;
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(RecentProjects::register).detach();
@ -26,6 +27,8 @@ pub struct RecentProjects {
picker: View<Picker<RecentProjectsDelegate>>,
}
impl ModalView for RecentProjects {}
impl RecentProjects {
fn new(delegate: RecentProjectsDelegate, cx: &mut ViewContext<Self>) -> Self {
Self {

View file

@ -1,13 +1,16 @@
use std::{ops::Range, sync::Arc};
use anyhow::bail;
use futures::FutureExt;
use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle};
use gpui::{
AnyElement, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText, IntoElement,
SharedString, StyledText, UnderlineStyle, WindowContext,
};
use language::{HighlightId, Language, LanguageRegistry};
use std::{ops::Range, sync::Arc};
use theme::ActiveTheme;
use util::RangeExt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Highlight {
Code,
Id(HighlightId),
Highlight(HighlightStyle),
Mention,
@ -28,24 +31,10 @@ impl From<HighlightId> for Highlight {
#[derive(Debug, Clone)]
pub struct RichText {
pub text: String,
pub text: SharedString,
pub highlights: Vec<(Range<usize>, Highlight)>,
pub region_ranges: Vec<Range<usize>>,
pub regions: Vec<RenderedRegion>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BackgroundKind {
Code,
/// A mention background for non-self user.
Mention,
SelfMention,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderedRegion {
pub background_kind: Option<BackgroundKind>,
pub link_url: Option<String>,
pub link_ranges: Vec<Range<usize>>,
pub link_urls: Arc<[String]>,
}
/// Allows one to specify extra links to the rendered markdown, which can be used
@ -56,94 +45,71 @@ pub struct Mention {
}
impl RichText {
pub fn element(
&self,
// syntax: Arc<SyntaxTheme>,
// style: RichTextStyle,
// cx: &mut ViewContext<V>,
) -> AnyElement {
todo!();
pub fn element(&self, id: ElementId, cx: &mut WindowContext) -> AnyElement {
let theme = cx.theme();
let code_background = theme.colors().surface_background;
// let mut region_id = 0;
// let view_id = cx.view_id();
InteractiveText::new(
id,
StyledText::new(self.text.clone()).with_highlights(
&cx.text_style(),
self.highlights.iter().map(|(range, highlight)| {
(
range.clone(),
match highlight {
Highlight::Code => HighlightStyle {
background_color: Some(code_background),
..Default::default()
},
Highlight::Id(id) => HighlightStyle {
background_color: Some(code_background),
..id.style(&theme.syntax()).unwrap_or_default()
},
Highlight::Highlight(highlight) => *highlight,
Highlight::Mention => HighlightStyle {
font_weight: Some(FontWeight::BOLD),
..Default::default()
},
Highlight::SelfMention => HighlightStyle {
font_weight: Some(FontWeight::BOLD),
..Default::default()
},
},
)
}),
),
)
.on_click(self.link_ranges.clone(), {
let link_urls = self.link_urls.clone();
move |ix, cx| cx.open_url(&link_urls[ix])
})
.into_any_element()
}
// let regions = self.regions.clone();
// enum Markdown {}
// Text::new(self.text.clone(), style.text.clone())
// .with_highlights(
// self.highlights
// .iter()
// .filter_map(|(range, highlight)| {
// let style = match highlight {
// Highlight::Id(id) => id.style(&syntax)?,
// Highlight::Highlight(style) => style.clone(),
// Highlight::Mention => style.mention_highlight,
// Highlight::SelfMention => style.self_mention_highlight,
// };
// Some((range.clone(), style))
// })
// .collect::<Vec<_>>(),
// )
// .with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| {
// region_id += 1;
// let region = regions[ix].clone();
// if let Some(url) = region.link_url {
// cx.scene().push_cursor_region(CursorRegion {
// bounds,
// style: CursorStyle::PointingHand,
// });
// cx.scene().push_mouse_region(
// MouseRegion::new::<Markdown>(view_id, region_id, bounds)
// .on_click::<V, _>(MouseButton::Left, move |_, _, cx| {
// cx.platform().open_url(&url)
// }),
// pub fn add_mention(
// &mut self,
// range: Range<usize>,
// is_current_user: bool,
// mention_style: HighlightStyle,
// ) -> anyhow::Result<()> {
// if range.end > self.text.len() {
// bail!(
// "Mention in range {range:?} is outside of bounds for a message of length {}",
// self.text.len()
// );
// }
// if let Some(region_kind) = &region.background_kind {
// let background = match region_kind {
// BackgroundKind::Code => style.code_background,
// BackgroundKind::Mention => style.mention_background,
// BackgroundKind::SelfMention => style.self_mention_background,
// };
// if background.is_some() {
// cx.scene().push_quad(gpui::Quad {
// bounds,
// background,
// border: Default::default(),
// corner_radii: (2.0).into(),
// if is_current_user {
// self.region_ranges.push(range.clone());
// self.regions.push(RenderedRegion {
// background_kind: Some(BackgroundKind::Mention),
// link_url: None,
// });
// }
// self.highlights
// .push((range, Highlight::Highlight(mention_style)));
// Ok(())
// }
// })
// .with_soft_wrap(true)
// .into_any()
}
pub fn add_mention(
&mut self,
range: Range<usize>,
is_current_user: bool,
mention_style: HighlightStyle,
) -> anyhow::Result<()> {
if range.end > self.text.len() {
bail!(
"Mention in range {range:?} is outside of bounds for a message of length {}",
self.text.len()
);
}
if is_current_user {
self.region_ranges.push(range.clone());
self.regions.push(RenderedRegion {
background_kind: Some(BackgroundKind::Mention),
link_url: None,
});
}
self.highlights
.push((range, Highlight::Highlight(mention_style)));
Ok(())
}
}
pub fn render_markdown_mut(
@ -151,7 +117,10 @@ pub fn render_markdown_mut(
mut mentions: &[Mention],
language_registry: &Arc<LanguageRegistry>,
language: Option<&Arc<Language>>,
data: &mut RichText,
text: &mut String,
highlights: &mut Vec<(Range<usize>, Highlight)>,
link_ranges: &mut Vec<Range<usize>>,
link_urls: &mut Vec<String>,
) {
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
@ -163,18 +132,18 @@ pub fn render_markdown_mut(
let options = Options::all();
for (event, source_range) in Parser::new_ext(&block, options).into_offset_iter() {
let prev_len = data.text.len();
let prev_len = text.len();
match event {
Event::Text(t) => {
if let Some(language) = &current_language {
render_code(&mut data.text, &mut data.highlights, t.as_ref(), language);
render_code(text, highlights, t.as_ref(), language);
} else {
if let Some(mention) = mentions.first() {
if source_range.contains_inclusive(&mention.range) {
mentions = &mentions[1..];
let range = (prev_len + mention.range.start - source_range.start)
..(prev_len + mention.range.end - source_range.start);
data.highlights.push((
highlights.push((
range.clone(),
if mention.is_self_mention {
Highlight::SelfMention
@ -182,19 +151,10 @@ pub fn render_markdown_mut(
Highlight::Mention
},
));
data.region_ranges.push(range);
data.regions.push(RenderedRegion {
background_kind: Some(if mention.is_self_mention {
BackgroundKind::SelfMention
} else {
BackgroundKind::Mention
}),
link_url: None,
});
}
}
data.text.push_str(t.as_ref());
text.push_str(t.as_ref());
let mut style = HighlightStyle::default();
if bold_depth > 0 {
style.font_weight = Some(FontWeight::BOLD);
@ -203,11 +163,8 @@ pub fn render_markdown_mut(
style.font_style = Some(FontStyle::Italic);
}
if let Some(link_url) = link_url.clone() {
data.region_ranges.push(prev_len..data.text.len());
data.regions.push(RenderedRegion {
link_url: Some(link_url),
background_kind: None,
});
link_ranges.push(prev_len..text.len());
link_urls.push(link_url);
style.underline = Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
@ -216,27 +173,25 @@ pub fn render_markdown_mut(
if style != HighlightStyle::default() {
let mut new_highlight = true;
if let Some((last_range, last_style)) = data.highlights.last_mut() {
if let Some((last_range, last_style)) = highlights.last_mut() {
if last_range.end == prev_len
&& last_style == &Highlight::Highlight(style)
{
last_range.end = data.text.len();
last_range.end = text.len();
new_highlight = false;
}
}
if new_highlight {
data.highlights
.push((prev_len..data.text.len(), Highlight::Highlight(style)));
highlights.push((prev_len..text.len(), Highlight::Highlight(style)));
}
}
}
}
Event::Code(t) => {
data.text.push_str(t.as_ref());
data.region_ranges.push(prev_len..data.text.len());
text.push_str(t.as_ref());
if link_url.is_some() {
data.highlights.push((
prev_len..data.text.len(),
highlights.push((
prev_len..text.len(),
Highlight::Highlight(HighlightStyle {
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
@ -246,19 +201,19 @@ pub fn render_markdown_mut(
}),
));
}
data.regions.push(RenderedRegion {
background_kind: Some(BackgroundKind::Code),
link_url: link_url.clone(),
});
if let Some(link_url) = link_url.clone() {
link_ranges.push(prev_len..text.len());
link_urls.push(link_url);
}
}
Event::Start(tag) => match tag {
Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack),
Tag::Paragraph => new_paragraph(text, &mut list_stack),
Tag::Heading(_, _, _) => {
new_paragraph(&mut data.text, &mut list_stack);
new_paragraph(text, &mut list_stack);
bold_depth += 1;
}
Tag::CodeBlock(kind) => {
new_paragraph(&mut data.text, &mut list_stack);
new_paragraph(text, &mut list_stack);
current_language = if let CodeBlockKind::Fenced(language) = kind {
language_registry
.language_for_name(language.as_ref())
@ -278,18 +233,18 @@ pub fn render_markdown_mut(
let len = list_stack.len();
if let Some((list_number, has_content)) = list_stack.last_mut() {
*has_content = false;
if !data.text.is_empty() && !data.text.ends_with('\n') {
data.text.push('\n');
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
for _ in 0..len - 1 {
data.text.push_str(" ");
text.push_str(" ");
}
if let Some(number) = list_number {
data.text.push_str(&format!("{}. ", number));
text.push_str(&format!("{}. ", number));
*number += 1;
*has_content = false;
} else {
data.text.push_str("- ");
text.push_str("- ");
}
}
}
@ -304,8 +259,8 @@ pub fn render_markdown_mut(
Tag::List(_) => drop(list_stack.pop()),
_ => {}
},
Event::HardBreak => data.text.push('\n'),
Event::SoftBreak => data.text.push(' '),
Event::HardBreak => text.push('\n'),
Event::SoftBreak => text.push(' '),
_ => {}
}
}
@ -317,18 +272,35 @@ pub fn render_markdown(
language_registry: &Arc<LanguageRegistry>,
language: Option<&Arc<Language>>,
) -> RichText {
let mut data = RichText {
text: Default::default(),
highlights: Default::default(),
region_ranges: Default::default(),
regions: Default::default(),
};
// let mut data = RichText {
// text: Default::default(),
// highlights: Default::default(),
// region_ranges: Default::default(),
// regions: Default::default(),
// };
render_markdown_mut(&block, mentions, language_registry, language, &mut data);
let mut text = String::new();
let mut highlights = Vec::new();
let mut link_ranges = Vec::new();
let mut link_urls = Vec::new();
render_markdown_mut(
&block,
mentions,
language_registry,
language,
&mut text,
&mut highlights,
&mut link_ranges,
&mut link_urls,
);
text.truncate(text.trim_end().len());
data.text = data.text.trim().to_string();
data
RichText {
text: SharedString::from(text),
link_urls: link_urls.into(),
link_ranges,
highlights,
}
}
pub fn render_code(
@ -339,11 +311,19 @@ pub fn render_code(
) {
let prev_len = text.len();
text.push_str(content);
let mut offset = 0;
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
if range.start > offset {
highlights.push((prev_len + offset..prev_len + range.start, Highlight::Code));
}
highlights.push((
prev_len + range.start..prev_len + range.end,
Highlight::Id(highlight_id),
));
offset = range.end;
}
if offset < content.len() {
highlights.push((prev_len + offset..prev_len + content.len(), Highlight::Code));
}
}

View file

@ -178,7 +178,9 @@ message Envelope {
GetNotifications get_notifications = 150;
GetNotificationsResponse get_notifications_response = 151;
DeleteNotification delete_notification = 152;
MarkNotificationRead mark_notification_read = 153; // Current max
MarkNotificationRead mark_notification_read = 153;
LspExtExpandMacro lsp_ext_expand_macro = 154;
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; // Current max
}
}
@ -1619,3 +1621,14 @@ message Notification {
bool is_read = 6;
optional bool response = 7;
}
message LspExtExpandMacro {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
}
message LspExtExpandMacroResponse {
string name = 1;
string expansion = 2;
}

View file

@ -280,6 +280,8 @@ messages!(
(UpdateWorktree, Foreground),
(UpdateWorktreeSettings, Foreground),
(UsersResponse, Foreground),
(LspExtExpandMacro, Background),
(LspExtExpandMacroResponse, Background),
);
request_messages!(
@ -363,6 +365,7 @@ request_messages!(
(UpdateParticipantLocation, Ack),
(UpdateProject, Ack),
(UpdateWorktree, Ack),
(LspExtExpandMacro, LspExtExpandMacroResponse),
);
entity_messages!(
@ -415,6 +418,7 @@ entity_messages!(
UpdateProjectCollaborator,
UpdateWorktree,
UpdateWorktreeSettings,
LspExtExpandMacro,
);
entity_messages!(

View file

@ -178,7 +178,9 @@ message Envelope {
GetNotifications get_notifications = 150;
GetNotificationsResponse get_notifications_response = 151;
DeleteNotification delete_notification = 152;
MarkNotificationRead mark_notification_read = 153; // Current max
MarkNotificationRead mark_notification_read = 153;
LspExtExpandMacro lsp_ext_expand_macro = 154;
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; // Current max
}
}
@ -1619,3 +1621,14 @@ message Notification {
bool is_read = 6;
optional bool response = 7;
}
message LspExtExpandMacro {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
}
message LspExtExpandMacroResponse {
string name = 1;
string expansion = 2;
}

View file

@ -280,6 +280,8 @@ messages!(
(UpdateWorktree, Foreground),
(UpdateWorktreeSettings, Foreground),
(UsersResponse, Foreground),
(LspExtExpandMacro, Background),
(LspExtExpandMacroResponse, Background),
);
request_messages!(
@ -363,6 +365,7 @@ request_messages!(
(UpdateParticipantLocation, Ack),
(UpdateProject, Ack),
(UpdateWorktree, Ack),
(LspExtExpandMacro, LspExtExpandMacroResponse),
);
entity_messages!(
@ -415,6 +418,7 @@ entity_messages!(
UpdateProjectCollaborator,
UpdateWorktree,
UpdateWorktreeSettings,
LspExtExpandMacro,
);
entity_messages!(

View file

@ -1075,8 +1075,7 @@ impl ProjectSearchView {
});
}
// Re-activate the most recently activated search or the most recent if it has been closed.
// If no search exists in the workspace, create a new one.
// Add another search tab to the workspace.
fn deploy(
workspace: &mut Workspace,
_: &workspace::NewSearch,
@ -1087,19 +1086,6 @@ impl ProjectSearchView {
state.0.retain(|project, _| project.is_upgradable(cx))
});
let active_search = cx
.global::<ActiveSearches>()
.0
.get(&workspace.project().downgrade());
let existing = active_search
.and_then(|active_search| {
workspace
.items_of_type::<ProjectSearchView>(cx)
.find(|search| search == active_search)
})
.or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
let query = workspace.active_item(cx).and_then(|item| {
let editor = item.act_as::<Editor>(cx)?;
let query = editor.query_suggestion(cx);
@ -1110,10 +1096,6 @@ impl ProjectSearchView {
}
});
let search = if let Some(existing) = existing {
workspace.activate_item(&existing, cx);
existing
} else {
let settings = cx
.global::<ActiveSettings>()
.0
@ -1126,11 +1108,9 @@ impl ProjectSearchView {
};
let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
let view = cx.add_view(|cx| ProjectSearchView::new(model, cx, settings));
let search = cx.add_view(|cx| ProjectSearchView::new(model, cx, settings));
workspace.add_item(Box::new(view.clone()), cx);
view
};
workspace.add_item(Box::new(search.clone()), cx);
search.update(cx, |search, cx| {
if let Some(query) = query {
@ -2306,29 +2286,86 @@ pub mod tests {
workspace.update(cx, |workspace, cx| {
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
});
deterministic.run_until_parked();
let Some(search_view_2) = cx.read(|cx| {
workspace
.read(cx)
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
}) else {
panic!("Search view expected to appear after new search event trigger")
};
let search_view_id_2 = search_view_2.id();
assert_ne!(
search_view_2, search_view,
"New search view should be open after `workspace::NewSearch` event"
);
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query");
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
"Results should be unchanged after search view 2nd open in a row"
"Results of the first search view should not update too"
);
assert!(
search_view.query_editor.is_focused(cx),
"Focus should be moved into query editor again after search view 2nd open in a row"
!search_view.query_editor.is_focused(cx),
"Focus should be moved away from the first search view"
);
});
search_view_2.update(cx, |search_view_2, cx| {
assert_eq!(
search_view_2.query_editor.read(cx).text(cx),
"two",
"New search view should get the query from the text cursor was at during the event spawn (first search view's first result)"
);
assert_eq!(
search_view_2
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"",
"No search results should be in the 2nd view yet, as we did not spawn a search for it"
);
assert!(
search_view_2.query_editor.is_focused(cx),
"Focus should be moved into query editor fo the new window"
);
});
search_view_2.update(cx, |search_view_2, cx| {
search_view_2
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("FOUR", cx));
search_view_2.search(cx);
});
deterministic.run_until_parked();
search_view_2.update(cx, |search_view_2, cx| {
assert_eq!(
search_view_2
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst FOUR: usize = one::ONE + three::THREE;",
"New search view with the updated query should have new search results"
);
assert!(
search_view_2.results_editor.is_focused(cx),
"Search view with mismatching query should be focused after search results are available",
);
});
cx.spawn(|mut cx| async move {
window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
window.dispatch_action(search_view_id_2, &ToggleFocus, &mut cx);
})
.detach();
deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| {
search_view_2.update(cx, |search_view_2, cx| {
assert!(
search_view.results_editor.is_focused(cx),
search_view_2.results_editor.is_focused(cx),
"Search view with matching query should switch focus to the results editor after the toggle focus event",
);
});

View file

@ -10,15 +10,15 @@ use collections::HashMap;
use editor::{Editor, EditorMode};
use futures::channel::oneshot;
use gpui::{
actions, div, red, Action, AppContext, Div, EventEmitter, FocusableView,
InteractiveElement as _, IntoElement, ParentElement as _, Render, Styled, Subscription, Task,
View, ViewContext, VisualContext as _, WeakView, WindowContext,
actions, div, impl_actions, red, Action, AppContext, Div, EventEmitter, FocusableView,
InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, Styled,
Subscription, Task, View, ViewContext, VisualContext as _, WeakView, WindowContext,
};
use project::search::SearchQuery;
use serde::Deserialize;
use std::{any::Any, sync::Arc};
use ui::{h_stack, Clickable, Icon, IconButton, IconElement};
use ui::{h_stack, ButtonCommon, Clickable, Icon, IconButton, IconElement, Tooltip};
use util::ResultExt;
use workspace::{
item::ItemHandle,
@ -26,12 +26,14 @@ use workspace::{
ToolbarItemLocation, ToolbarItemView,
};
#[derive(PartialEq, Clone, Deserialize, Default, Action)]
#[derive(PartialEq, Clone, Deserialize)]
pub struct Deploy {
pub focus: bool,
}
actions!(Dismiss, FocusEditor);
impl_actions!(buffer_search, [Deploy]);
actions!(buffer_search, [Dismiss, FocusEditor]);
pub enum Event {
UpdateLocation,
@ -131,13 +133,7 @@ impl Render for BufferSearchBar {
let search_button_for_mode = |mode| {
let is_active = self.current_mode == mode;
render_search_mode_button(
mode,
is_active,
cx.listener(move |this, _, cx| {
this.activate_search_mode(mode, cx);
}),
)
render_search_mode_button(mode, is_active)
};
let search_option_button = |option| {
let is_active = self.search_options.contains(option);
@ -163,23 +159,35 @@ impl Render for BufferSearchBar {
});
let should_show_replace_input = self.replace_enabled && supported_options.replacement;
let replace_all = should_show_replace_input
.then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll));
let replace_next = should_show_replace_input
.then(|| super::render_replace_button(ReplaceNext, ui::Icon::ReplaceNext));
.then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll, "Replace all"));
let replace_next = should_show_replace_input.then(|| {
super::render_replace_button(ReplaceNext, ui::Icon::ReplaceNext, "Replace next")
});
let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
let mut key_context = KeyContext::default();
key_context.add("BufferSearchBar");
if in_replace {
key_context.add("in_replace");
}
h_stack()
.key_context("BufferSearchBar")
.key_context(key_context)
.on_action(cx.listener(Self::previous_history_query))
.on_action(cx.listener(Self::next_history_query))
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::select_next_match))
.on_action(cx.listener(Self::select_prev_match))
.on_action(cx.listener(|this, _: &ActivateRegexMode, cx| {
this.activate_search_mode(SearchMode::Regex, cx);
}))
.on_action(cx.listener(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx);
}))
.when(self.supported_options().replacement, |this| {
this.on_action(cx.listener(Self::toggle_replace))
.when(in_replace, |this| {
this.key_context("in_replace")
.on_action(cx.listener(Self::replace_next))
this.on_action(cx.listener(Self::replace_next))
.on_action(cx.listener(Self::replace_all))
})
})
@ -238,21 +246,19 @@ impl Render for BufferSearchBar {
h_stack()
.gap_0p5()
.flex_none()
.child(self.render_action_button(cx))
.child(self.render_action_button())
.children(match_count)
.child(render_nav_button(
ui::Icon::ChevronLeft,
self.active_match_index.is_some(),
cx.listener(move |this, _, cx| {
this.select_prev_match(&Default::default(), cx);
}),
"Select previous match",
&SelectPrevMatch,
))
.child(render_nav_button(
ui::Icon::ChevronRight,
self.active_match_index.is_some(),
cx.listener(move |this, _, cx| {
this.select_next_match(&Default::default(), cx);
}),
"Select next match",
&SelectNextMatch,
)),
)
}
@ -597,14 +603,10 @@ impl BufferSearchBar {
self.update_matches(cx)
}
fn render_action_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
// let tooltip_style = theme.tooltip.clone();
// let style = theme.search.action_button.clone();
IconButton::new("select-all", ui::Icon::SelectAll).on_click(cx.listener(|this, _, cx| {
this.select_all_matches(&SelectAllMatches, cx);
}))
fn render_action_button(&self) -> impl IntoElement {
IconButton::new("select-all", ui::Icon::SelectAll)
.on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
.tooltip(|cx| Tooltip::for_action("Select all matches", &SelectAllMatches, cx))
}
pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {

View file

@ -1,3 +1,7 @@
use gpui::{Action, SharedString};
use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode};
// TODO: Update the default search mode to get from config
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub enum SearchMode {
@ -15,6 +19,16 @@ impl SearchMode {
SearchMode::Regex => "Regex",
}
}
pub(crate) fn tooltip(&self) -> SharedString {
format!("Activate {} Mode", self.label()).into()
}
pub(crate) fn action(&self) -> Box<dyn Action> {
match self {
SearchMode::Text => ActivateTextMode.boxed_clone(),
SearchMode::Semantic => ActivateSemanticMode.boxed_clone(),
SearchMode::Regex => ActivateRegexMode.boxed_clone(),
}
}
}
pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {

View file

@ -1,8 +1,7 @@
use crate::{
history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateSemanticMode,
ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext,
SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace,
ToggleWholeWord,
history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode,
NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
};
use anyhow::{Context as _, Result};
use collections::HashMap;
@ -45,7 +44,10 @@ use workspace::{
WorkspaceId,
};
actions!(SearchInNew, ToggleFocus, NextField, ToggleFilters,);
actions!(
project_search,
[SearchInNew, ToggleFocus, NextField, ToggleFilters]
);
#[derive(Default)]
struct ActiveSearches(HashMap<WeakModel<Project>, WeakView<ProjectSearchView>>);
@ -58,7 +60,9 @@ pub fn init(cx: &mut AppContext) {
cx.set_global(ActiveSearches::default());
cx.set_global(ActiveSettings::default());
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(ProjectSearchView::deploy);
workspace
.register_action(ProjectSearchView::deploy)
.register_action(ProjectSearchBar::search_in_new);
})
.detach();
}
@ -1007,8 +1011,7 @@ impl ProjectSearchView {
});
}
// Re-activate the most recently activated search or the most recent if it has been closed.
// If no search exists in the workspace, create a new one.
// Add another search tab to the workspace.
fn deploy(
workspace: &mut Workspace,
_: &workspace::NewSearch,
@ -1019,20 +1022,6 @@ impl ProjectSearchView {
state.0.retain(|project, _| project.is_upgradable())
});
let active_search = cx
.global::<ActiveSearches>()
.0
.get(&workspace.project().downgrade())
.and_then(WeakView::upgrade);
let existing = active_search
.and_then(|active_search| {
workspace
.items_of_type::<ProjectSearchView>(cx)
.find(|search| search == &active_search)
})
.or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
let query = workspace.active_item(cx).and_then(|item| {
let editor = item.act_as::<Editor>(cx)?;
let query = editor.query_suggestion(cx);
@ -1043,10 +1032,6 @@ impl ProjectSearchView {
}
});
let search = if let Some(existing) = existing {
workspace.activate_item(&existing, cx);
existing
} else {
let settings = cx
.global::<ActiveSettings>()
.0
@ -1059,11 +1044,9 @@ impl ProjectSearchView {
};
let model = cx.build_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
let view = cx.build_view(|cx| ProjectSearchView::new(model, cx, settings));
let search = cx.build_view(|cx| ProjectSearchView::new(model, cx, settings));
workspace.add_item(Box::new(view.clone()), cx);
view
};
workspace.add_item(Box::new(search.clone()), cx);
search.update(cx, |search, cx| {
if let Some(query) = query {
@ -2436,29 +2419,86 @@ pub mod tests {
workspace.update(cx, |workspace, cx| {
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
});
deterministic.run_until_parked();
let Some(search_view_2) = cx.read(|cx| {
workspace
.read(cx)
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
}) else {
panic!("Search view expected to appear after new search event trigger")
};
let search_view_id_2 = search_view_2.id();
assert_ne!(
search_view_2, search_view,
"New search view should be open after `workspace::NewSearch` event"
);
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query");
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
"Results should be unchanged after search view 2nd open in a row"
"Results of the first search view should not update too"
);
assert!(
search_view.query_editor.is_focused(cx),
"Focus should be moved into query editor again after search view 2nd open in a row"
!search_view.query_editor.is_focused(cx),
"Focus should be moved away from the first search view"
);
});
search_view_2.update(cx, |search_view_2, cx| {
assert_eq!(
search_view_2.query_editor.read(cx).text(cx),
"two",
"New search view should get the query from the text cursor was at during the event spawn (first search view's first result)"
);
assert_eq!(
search_view_2
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"",
"No search results should be in the 2nd view yet, as we did not spawn a search for it"
);
assert!(
search_view_2.query_editor.is_focused(cx),
"Focus should be moved into query editor fo the new window"
);
});
search_view_2.update(cx, |search_view_2, cx| {
search_view_2
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("FOUR", cx));
search_view_2.search(cx);
});
deterministic.run_until_parked();
search_view_2.update(cx, |search_view_2, cx| {
assert_eq!(
search_view_2
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst FOUR: usize = one::ONE + three::THREE;",
"New search view with the updated query should have new search results"
);
assert!(
search_view_2.results_editor.is_focused(cx),
"Search view with mismatching query should be focused after search results are available",
);
});
cx.spawn(|mut cx| async move {
window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
window.dispatch_action(search_view_id_2, &ToggleFocus, &mut cx);
})
.detach();
deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| {
search_view_id_2.update(cx, |search_view_2, cx| {
assert!(
search_view.results_editor.is_focused(cx),
search_view_2.results_editor.is_focused(cx),
"Search view with matching query should switch focus to the results editor after the toggle focus event",
);
});

View file

@ -3,7 +3,7 @@ pub use buffer_search::BufferSearchBar;
use gpui::{actions, Action, AppContext, IntoElement};
pub use mode::SearchMode;
use project::search::SearchQuery;
use ui::prelude::*;
use ui::{prelude::*, Tooltip};
use ui::{ButtonStyle, Icon, IconButton};
//pub use project_search::{ProjectSearchBar, ProjectSearchView};
// use theme::components::{
@ -22,6 +22,8 @@ pub fn init(cx: &mut AppContext) {
}
actions!(
search,
[
CycleMode,
ToggleWholeWord,
ToggleCaseSensitive,
@ -36,6 +38,7 @@ actions!(
ActivateRegexMode,
ReplaceAll,
ReplaceNext,
]
);
bitflags! {
@ -85,7 +88,7 @@ impl SearchOptions {
}
pub fn as_button(&self, active: bool) -> impl IntoElement {
IconButton::new(0, self.icon())
IconButton::new(self.label(), self.icon())
.on_click({
let action = self.to_toggle_action();
move |_, cx| {
@ -94,26 +97,38 @@ impl SearchOptions {
})
.style(ButtonStyle::Subtle)
.when(active, |button| button.style(ButtonStyle::Filled))
.tooltip({
let action = self.to_toggle_action();
let label: SharedString = format!("Toggle {}", self.label()).into();
move |cx| Tooltip::for_action(label.clone(), &*action, cx)
})
}
}
fn toggle_replace_button(active: bool) -> impl IntoElement {
// todo: add toggle_replace button
IconButton::new(0, Icon::Replace)
IconButton::new("buffer-search-bar-toggle-replace-button", Icon::Replace)
.on_click(|_, cx| {
cx.dispatch_action(Box::new(ToggleReplace));
cx.notify();
})
.style(ButtonStyle::Subtle)
.when(active, |button| button.style(ButtonStyle::Filled))
.tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx))
}
fn render_replace_button(
action: impl Action + 'static + Send + Sync,
icon: Icon,
tooltip: &'static str,
) -> impl IntoElement {
// todo: add tooltip
IconButton::new(0, icon).on_click(move |_, cx| {
let id: SharedString = format!("search-replace-{}", action.name()).into();
IconButton::new(id, icon)
.tooltip({
let action = action.boxed_clone();
move |cx| Tooltip::for_action(tooltip, &*action, cx)
})
.on_click(move |_, cx| {
cx.dispatch_action(action.boxed_clone());
})
}

View file

@ -1,30 +1,36 @@
use gpui::{ClickEvent, IntoElement, WindowContext};
use ui::prelude::*;
use gpui::{Action, IntoElement};
use ui::{prelude::*, Tooltip};
use ui::{Button, IconButton};
use crate::mode::SearchMode;
pub(super) fn render_nav_button(
icon: ui::Icon,
_active: bool,
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
active: bool,
tooltip: &'static str,
action: &'static dyn Action,
) -> impl IntoElement {
// let tooltip_style = cx.theme().tooltip.clone();
// let cursor_style = if active {
// CursorStyle::PointingHand
// } else {
// CursorStyle::default()
// };
// enum NavButton {}
IconButton::new("search-nav-button", icon).on_click(on_click)
IconButton::new(
SharedString::from(format!("search-nav-button-{}", action.name())),
icon,
)
.on_click(|_, cx| cx.dispatch_action(action.boxed_clone()))
.tooltip(move |cx| Tooltip::for_action(tooltip, action, cx))
.disabled(!active)
}
pub(crate) fn render_search_mode_button(
mode: SearchMode,
is_active: bool,
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
) -> Button {
pub(crate) fn render_search_mode_button(mode: SearchMode, is_active: bool) -> Button {
Button::new(mode.label(), mode.label())
.selected(is_active)
.on_click(on_click)
.on_click({
let action = mode.action();
move |_, cx| {
cx.dispatch_action(action.boxed_clone());
}
})
.tooltip({
let action = mode.action();
let tooltip_text = mode.tooltip();
move |cx| Tooltip::for_action(tooltip_text.clone(), &*action, cx)
})
}

View file

@ -1,7 +1,7 @@
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
use anyhow::{anyhow, Context, Result};
use collections::BTreeMap;
use gpui::{actions, Action, AppContext, KeyBinding, SharedString};
use gpui::{Action, AppContext, KeyBinding, SharedString};
use schemars::{
gen::{SchemaGenerator, SchemaSettings},
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
@ -137,10 +137,8 @@ impl KeymapFile {
}
}
actions!(NoAction);
fn no_action() -> Box<dyn gpui::Action> {
NoAction.boxed_clone()
gpui::NoAction.boxed_clone()
}
#[cfg(test)]

View file

@ -4,7 +4,7 @@ use gpui::{
};
use ui::prelude::*;
actions!(ActionA, ActionB, ActionC);
actions!(focus, [ActionA, ActionB, ActionC]);
pub struct FocusStory {
child_1_focus: FocusHandle,

View file

@ -28,6 +28,7 @@ pub enum ComponentStory {
ListHeader,
ListItem,
Scroll,
Tab,
Text,
ZIndex,
Picker,
@ -53,6 +54,7 @@ impl ComponentStory {
Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
Self::Scroll => ScrollStory::view(cx).into(),
Self::Text => TextStory::view(cx).into(),
Self::Tab => cx.build_view(|_| ui::TabStory).into(),
Self::ZIndex => cx.build_view(|_| ZIndexStory).into(),
Self::Picker => PickerStory::new(cx).into(),
}

View file

@ -50,7 +50,7 @@ use std::{
use thiserror::Error;
use gpui::{
px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla, Keystroke,
actions, px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla, Keystroke,
ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Point, ScrollWheelEvent, Size, Task, TouchPhase,
};
@ -58,6 +58,19 @@ use gpui::{
use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str};
use lazy_static::lazy_static;
actions!(
terminal,
[
Clear,
Copy,
Paste,
ShowCharacterPalette,
SearchTest,
SendText,
SendKeystroke,
]
);
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars.

View file

@ -1,11 +1,11 @@
use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
use gpui::{
black, div, point, px, red, relative, transparent_black, AnyElement, AsyncWindowContext,
AvailableSpace, Bounds, DispatchPhase, Element, ElementId, FocusHandle, Font, FontStyle,
FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, IntoElement,
LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels,
AvailableSpace, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font,
FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState,
IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels,
PlatformInputHandler, Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled,
TextRun, TextStyle, TextSystem, UnderlineStyle, View, WhiteSpace, WindowContext,
TextRun, TextStyle, TextSystem, UnderlineStyle, WhiteSpace, WindowContext,
};
use itertools::Itertools;
use language::CursorShape;
@ -27,8 +27,6 @@ use ui::Tooltip;
use std::mem;
use std::{fmt::Debug, ops::RangeInclusive};
use crate::TerminalView;
///The information generated during layout that is necessary for painting
pub struct LayoutState {
cells: Vec<LayoutCell>,
@ -149,7 +147,6 @@ impl LayoutRect {
///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
pub struct TerminalElement {
terminal: Model<Terminal>,
terminal_view: View<TerminalView>,
focus: FocusHandle,
focused: bool,
cursor_visible: bool,
@ -168,7 +165,6 @@ impl StatefulInteractiveElement for TerminalElement {}
impl TerminalElement {
pub fn new(
terminal: Model<Terminal>,
terminal_view: View<TerminalView>,
focus: FocusHandle,
focused: bool,
cursor_visible: bool,
@ -176,7 +172,6 @@ impl TerminalElement {
) -> TerminalElement {
TerminalElement {
terminal,
terminal_view,
focused,
focus: focus.clone(),
cursor_visible,
@ -474,6 +469,7 @@ impl TerminalElement {
.size_full()
.id("terminal-element")
.tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx))
.into_any_element()
});
let TerminalContent {
@ -575,7 +571,7 @@ impl TerminalElement {
relative_highlighted_ranges,
mode: *mode,
display_offset: *display_offset,
hyperlink_tooltip: None, // todo!(tooltips)
hyperlink_tooltip,
gutter,
}
}
@ -643,15 +639,13 @@ impl TerminalElement {
let connection = connection.clone();
let focus = focus.clone();
move |e, cx| {
if e.pressed_button.is_some() {
if focus.is_focused(cx) {
if e.pressed_button.is_some() && focus.is_focused(cx) && !cx.has_active_drag() {
connection.update(cx, |terminal, cx| {
terminal.mouse_drag(e, origin, bounds);
cx.notify();
})
}
}
}
})
.on_mouse_up(
MouseButton::Left,
@ -776,18 +770,11 @@ impl Element for TerminalElement {
(layout_id, interactive_state)
}
fn paint(
mut self,
bounds: Bounds<Pixels>,
state: &mut Self::State,
cx: &mut WindowContext<'_>,
) {
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext<'_>) {
let mut layout = self.compute_layout(bounds, cx);
let theme = cx.theme();
let dispatch_context = self.terminal_view.read(cx).dispatch_context(cx);
self.interactivity().key_context = Some(dispatch_context);
cx.paint_quad(
bounds,
Default::default(),
@ -806,7 +793,28 @@ impl Element for TerminalElement {
.map(|cursor| cursor.bounding_rect(origin)),
};
let mut this = self.register_mouse_listeners(origin, layout.mode, bounds, cx);
let terminal_focus_handle = self.focus.clone();
let terminal_handle = self.terminal.clone();
let mut this: TerminalElement = self
.register_mouse_listeners(origin, layout.mode, bounds, cx)
.drag_over::<ExternalPaths>(|style| {
// todo!() why does not it work? z-index of elements?
style.bg(cx.theme().colors().ghost_element_hover)
})
.on_drop::<ExternalPaths>(move |external_paths, cx| {
cx.focus(&terminal_focus_handle);
let mut new_text = external_paths
.read(cx)
.paths()
.iter()
.map(|path| format!(" {path:?}"))
.join("");
new_text.push(' ');
terminal_handle.update(cx, |terminal, _| {
// todo!() long paths are not displayed properly albeit the text is there
terminal.paste(&new_text);
});
});
let interactivity = mem::take(&mut this.interactivity);

View file

@ -24,7 +24,7 @@ use anyhow::Result;
const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
actions!(ToggleFocus);
actions!(terminal_view, [ToggleFocus]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(

View file

@ -9,9 +9,10 @@ pub mod terminal_panel;
// use crate::terminal_element::TerminalElement;
use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{
actions, div, Action, AnyElement, AppContext, Div, EventEmitter, FocusEvent, FocusHandle,
Focusable, FocusableElement, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model,
MouseButton, MouseDownEvent, Pixels, Render, Subscription, Task, View, VisualContext, WeakView,
div, impl_actions, overlay, AnyElement, AppContext, DismissEvent, Div, EventEmitter,
FocusEvent, FocusHandle, Focusable, FocusableElement, FocusableView, KeyContext, KeyDownEvent,
Keystroke, Model, MouseButton, MouseDownEvent, Pixels, Render, Styled, Subscription, Task,
View, VisualContext, WeakView,
};
use language::Bias;
use persistence::TERMINAL_DB;
@ -22,7 +23,7 @@ use terminal::{
term::{search::RegexSearch, TermMode},
},
terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
Event, MaybeNavigationTarget, Terminal,
Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal,
};
use terminal_element::TerminalElement;
use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label};
@ -54,13 +55,13 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Clone, Debug, PartialEq)]
pub struct ScrollTerminal(pub i32);
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
pub struct SendText(String);
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
pub struct SendKeystroke(String);
actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest);
impl_actions!(terminal_view, [SendText, SendKeystroke]);
pub fn init(cx: &mut AppContext) {
terminal_panel::init(cx);
@ -83,7 +84,7 @@ pub struct TerminalView {
has_new_content: bool,
//Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool,
context_menu: Option<View<ContextMenu>>,
context_menu: Option<(View<ContextMenu>, gpui::Point<Pixels>, Subscription)>,
blink_state: bool,
blinking_on: bool,
blinking_paused: bool,
@ -314,14 +315,24 @@ impl TerminalView {
position: gpui::Point<Pixels>,
cx: &mut ViewContext<Self>,
) {
self.context_menu = Some(ContextMenu::build(cx, |menu, cx| {
let context_menu = ContextMenu::build(cx, |menu, cx| {
menu.action("Clear", Box::new(Clear))
.action("Close", Box::new(CloseActiveItem { save_intent: None }))
}));
// todo!(context menus)
// self.context_menu
// .show(position, AnchorCorner::TopLeft, menu_entries, cx);
// cx.notify();
});
cx.focus_view(&context_menu);
let subscription =
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
if this.context_menu.as_ref().is_some_and(|context_menu| {
context_menu.0.focus_handle(cx).contains_focused(cx)
}) {
cx.focus_self();
}
this.context_menu.take();
cx.notify();
});
self.context_menu = Some((context_menu, position, subscription));
}
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
@ -623,7 +634,6 @@ impl Render for TerminalView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let terminal_handle = self.terminal.clone();
let this_view = cx.view().clone();
let self_id = cx.entity_id();
let focused = self.focus_handle.is_focused(cx);
@ -631,12 +641,8 @@ impl Render for TerminalView {
div()
.size_full()
.relative()
.child(
div()
.z_index(0)
.absolute()
.size_full()
.on_key_down(cx.listener(Self::key_down))
.track_focus(&self.focus_handle)
.key_context(self.dispatch_context(cx))
.on_action(cx.listener(TerminalView::send_text))
.on_action(cx.listener(TerminalView::send_keystroke))
.on_action(cx.listener(TerminalView::copy))
@ -644,30 +650,32 @@ impl Render for TerminalView {
.on_action(cx.listener(TerminalView::clear))
.on_action(cx.listener(TerminalView::show_character_palette))
.on_action(cx.listener(TerminalView::select_all))
.child(TerminalElement::new(
terminal_handle,
this_view,
self.focus_handle.clone(),
focused,
self.should_show_cursor(focused, cx),
self.can_navigate_to_selected_word,
))
.on_focus_in(cx.listener(Self::focus_in))
.on_focus_out(cx.listener(Self::focus_out))
.on_key_down(cx.listener(Self::key_down))
.on_mouse_down(
MouseButton::Right,
cx.listener(|this, event: &MouseDownEvent, cx| {
this.deploy_context_menu(event.position, cx);
cx.notify();
}),
),
)
.children(
self.context_menu
.clone()
.map(|context_menu| div().z_index(1).absolute().child(context_menu)),
.child(
// TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu
div().size_full().child(TerminalElement::new(
terminal_handle,
self.focus_handle.clone(),
focused,
self.should_show_cursor(focused, cx),
self.can_navigate_to_selected_word,
)),
)
.track_focus(&self.focus_handle)
.on_focus_in(cx.listener(Self::focus_in))
.on_focus_out(cx.listener(Self::focus_out))
.children(self.context_menu.as_ref().map(|(menu, positon, _)| {
overlay()
.position(*positon)
.anchor(gpui::AnchorCorner::TopLeft)
.child(menu.clone())
}))
}
}

View file

@ -10,6 +10,12 @@ use crate::{
SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily,
};
#[derive(Debug, Clone)]
pub struct ThemeMeta {
pub name: SharedString,
pub appearance: Appearance,
}
pub struct ThemeRegistry {
themes: HashMap<SharedString, Arc<Theme>>,
}
@ -94,8 +100,11 @@ impl ThemeRegistry {
self.themes.keys().cloned()
}
pub fn list(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
self.themes.values().map(|theme| theme.name.clone())
pub fn list(&self, _staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
self.themes.values().map(|theme| ThemeMeta {
name: theme.name.clone(),
appearance: theme.appearance(),
})
}
pub fn get(&self, name: &str) -> Result<Arc<Theme>> {

View file

@ -31,6 +31,15 @@ pub enum Appearance {
Dark,
}
impl Appearance {
pub fn is_light(&self) -> bool {
match self {
Self::Light => true,
Self::Dark => false,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum LoadThemes {
/// Only load the base theme.

Some files were not shown because too many files have changed in this diff Show more